diff options
415 files changed, 4840 insertions, 2201 deletions
diff --git a/.travis.yml b/.travis.yml index 0038668ae7..6c870d8797 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,7 +20,7 @@ env: - "GEM=ar:postgresql" - "GEM=aj:integration" rvm: - - 2.2.1 + - 2.2.2 - ruby-head - rbx-2 - jruby-head @@ -17,10 +17,12 @@ gem 'turbolinks' gem 'arel', github: 'rails/arel', branch: 'master' gem 'mail', github: 'mikel/mail' +gem 'sprockets', '~> 3.0.0.rc.1' + # require: false so bcrypt is loaded only when has_secure_password is used. # This is to avoid ActiveModel (and by extension the entire framework) # being dependent on a binary library. -gem 'bcrypt', '~> 3.1.7', require: false +gem 'bcrypt', '~> 3.1.10', require: false # This needs to be with require false to avoid # it being automatically loaded by sprockets @@ -28,12 +30,12 @@ gem 'uglifier', '>= 1.3.0', require: false group :doc do gem 'sdoc', '~> 0.4.0' - gem 'redcarpet', '~> 3.2.2', platforms: :ruby + gem 'redcarpet', '~> 3.2.3', platforms: :ruby gem 'w3c_validators' gem 'kindlerb', '0.1.1' end -# AS +# ActiveSupport gem 'dalli', '>= 2.2.1' # ActiveJob @@ -61,18 +63,11 @@ group :test do # FIX: Our test suite isn't ready to run in random order yet gem 'minitest', '< 5.3.4' - platforms :mri_19 do - gem 'ruby-prof', '~> 0.11.2' - end - - platforms :mri_21, :mri_22 do + platforms :mri do gem 'stackprof' + # gem 'byebug' end - # platforms :mri do - # gem 'byebug' - # end - gem 'benchmark-ips' end @@ -82,7 +77,7 @@ platforms :ruby do # Needed for compiling the ActionDispatch::Journey parser gem 'racc', '>=1.4.6', require: false - # AR + # ActiveRecord gem 'sqlite3', '~> 1.3.6' group :db do diff --git a/Gemfile.lock b/Gemfile.lock index 0dc7559d9e..543cfaf3da 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -50,13 +50,13 @@ PATH rack (~> 1.6) rack-test (~> 0.6.3) rails-dom-testing (~> 1.0, >= 1.0.5) - rails-html-sanitizer (~> 1.0, >= 1.0.1) + rails-html-sanitizer (~> 1.0, >= 1.0.2) actionview (5.0.0.alpha) activesupport (= 5.0.0.alpha) builder (~> 3.1) erubis (~> 2.7.0) rails-dom-testing (~> 1.0, >= 1.0.5) - rails-html-sanitizer (~> 1.0, >= 1.0.1) + rails-html-sanitizer (~> 1.0, >= 1.0.2) activejob (5.0.0.alpha) activesupport (= 5.0.0.alpha) globalid (>= 0.3.0) @@ -87,6 +87,7 @@ PATH railties (5.0.0.alpha) actionpack (= 5.0.0.alpha) activesupport (= 5.0.0.alpha) + method_source rake (>= 0.8.7) thor (>= 0.18.1, < 2.0) @@ -98,6 +99,8 @@ GEM beaneater (~> 0.3.1) dante (~> 0.1.5) bcrypt (3.1.10) + bcrypt (3.1.10-x64-mingw32) + bcrypt (3.1.10-x86-mingw32) beaneater (0.3.3) benchmark-ips (2.1.1) builder (3.2.2) @@ -124,8 +127,8 @@ GEM execjs (2.3.0) globalid (0.3.3) activesupport (>= 4.1.0) - hike (1.2.3) hitimes (1.2.2) + hitimes (1.2.2-x86-mingw32) i18n (0.7.0) json (1.8.2) kindlerb (0.1.1) @@ -134,18 +137,23 @@ GEM loofah (2.0.1) nokogiri (>= 1.5.9) metaclass (0.0.4) + method_source (0.8.2) mime-types (2.4.3) mini_portile (0.6.2) minitest (5.3.3) mocha (0.14.0) metaclass (~> 0.0.1) mono_logger (1.1.0) - multi_json (1.10.1) + multi_json (1.11.0) mustache (1.0.0) mysql (2.9.1) mysql2 (0.3.18) nokogiri (1.6.6.2) mini_portile (~> 0.6.0) + nokogiri (1.6.6.2-x64-mingw32) + mini_portile (~> 0.6.0) + nokogiri (1.6.6.2-x86-mingw32) + mini_portile (~> 0.6.0) pg (0.18.1) psych (2.0.13) que (0.9.2) @@ -165,11 +173,11 @@ GEM activesupport (>= 4.2.0.beta, < 5.0) nokogiri (~> 1.6.0) rails-deprecated_sanitizer (>= 1.0.1) - rails-html-sanitizer (1.0.1) + rails-html-sanitizer (1.0.2) loofah (~> 2.0) rake (10.4.2) rdoc (4.2.0) - redcarpet (3.2.2) + redcarpet (3.2.3) redis (3.2.1) redis-namespace (1.5.1) redis (~> 3.0, >= 3.0.4) @@ -184,7 +192,6 @@ GEM redis (~> 3.0) resque (~> 1.25) rufus-scheduler (~> 3.0) - ruby-prof (0.11.3) rufus-scheduler (3.0.9) tzinfo sdoc (0.4.1) @@ -209,11 +216,8 @@ GEM serverengine thor thread - sprockets (2.12.3) - hike (~> 1.2) - multi_json (~> 1.0) + sprockets (3.0.0.rc.1) rack (~> 1.0) - tilt (~> 1.1, != 1.3.0) sprockets-rails (2.2.4) actionpack (>= 3.0) activesupport (>= 3.0) @@ -243,6 +247,8 @@ GEM PLATFORMS ruby + x64-mingw32 + x86-mingw32 DEPENDENCIES activerecord-jdbcmysql-adapter (>= 1.3.0) @@ -250,7 +256,7 @@ DEPENDENCIES activerecord-jdbcsqlite3-adapter (>= 1.3.0) arel! backburner - bcrypt (~> 3.1.7) + bcrypt (~> 3.1.10) benchmark-ips coffee-rails (~> 4.1.0) dalli (>= 2.2.1) @@ -275,14 +281,14 @@ DEPENDENCIES rack-cache (~> 1.2) rails! rake (>= 10.3) - redcarpet (~> 3.2.2) + redcarpet (~> 3.2.3) resque resque-scheduler - ruby-prof (~> 0.11.2) sdoc (~> 0.4.0) sequel sidekiq sneakers (= 0.1.1.pre) + sprockets (~> 3.0.0.rc.1) sqlite3 (~> 1.3.6) stackprof sucker_punch diff --git a/actionmailer/actionmailer.gemspec b/actionmailer/actionmailer.gemspec index 513c217733..782b208ef4 100644 --- a/actionmailer/actionmailer.gemspec +++ b/actionmailer/actionmailer.gemspec @@ -7,7 +7,7 @@ Gem::Specification.new do |s| s.summary = 'Email composition, delivery, and receiving framework (part of Rails).' s.description = 'Email on Rails. Compose, deliver, receive, and test emails using the familiar controller/view pattern. First-class support for multipart email and attachments.' - s.required_ruby_version = '>= 2.2.0' + s.required_ruby_version = '>= 2.2.2' s.license = 'MIT' diff --git a/actionmailer/lib/action_mailer/base.rb b/actionmailer/lib/action_mailer/base.rb index 3e5125f72e..6ddc4c9596 100644 --- a/actionmailer/lib/action_mailer/base.rb +++ b/actionmailer/lib/action_mailer/base.rb @@ -21,7 +21,7 @@ module ActionMailer # the mailer views, options on the mail itself such as the <tt>:from</tt> address, and attachments. # # class ApplicationMailer < ActionMailer::Base - # default from: 'from@exmaple.com' + # default from: 'from@example.com' # layout 'mailer' # end # @@ -134,25 +134,28 @@ module ActionMailer # # = Sending mail # - # Once a mailer action and template are defined, you can deliver your message or create it and save it - # for delivery later: + # Once a mailer action and template are defined, you can deliver your message or defer its creation and + # delivery for later: # # NotifierMailer.welcome(User.first).deliver_now # sends the email # mail = NotifierMailer.welcome(User.first) # => an ActionMailer::MessageDelivery object - # mail.deliver_now # sends the email + # mail.deliver_now # generates and sends the email now # - # The <tt>ActionMailer::MessageDelivery</tt> class is a wrapper around a <tt>Mail::Message</tt> object. If - # you want direct access to the <tt>Mail::Message</tt> object you can call the <tt>message</tt> method on - # the <tt>ActionMailer::MessageDelivery</tt> object. + # The <tt>ActionMailer::MessageDelivery</tt> class is a wrapper around a delegate that will call + # your method to generate the mail. If you want direct access to delegator, or <tt>Mail::Message</tt>, + # you can call the <tt>message</tt> method on the <tt>ActionMailer::MessageDelivery</tt> object. # # NotifierMailer.welcome(User.first).message # => a Mail::Message object # - # Action Mailer is nicely integrated with Active Job so you can send emails in the background (example: outside - # of the request-response cycle, so the user doesn't have to wait on it): + # Action Mailer is nicely integrated with Active Job so you can generate and send emails in the background + # (example: outside of the request-response cycle, so the user doesn't have to wait on it): # # NotifierMailer.welcome(User.first).deliver_later # enqueue the email sending to Active Job # + # Note that <tt>deliver_later</tt> will execute your method from the background job. + # # You never instantiate your mailer class. Rather, you just call the method you defined on the class itself. + # All instance method are expected to return a message object to be sent. # # = Multipart Emails # diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md index 108ebfda58..0a31e34d3d 100644 --- a/actionpack/CHANGELOG.md +++ b/actionpack/CHANGELOG.md @@ -1,6 +1,51 @@ +* Add ability to override default form builder for a controller. + + class AdminController < ApplicationController + default_form_builder AdminFormBuilder + end + + *Kevin McPhillips* + +* For actions with no corresponding templates, render `head :no_content` + instead of raising an error. This allows for slimmer API controller + methods that simply work, without needing further instructions. + + See #19036. + + *Stephen Bussey* + +* Provide friendlier access to request variants. + + request.variant = :phone + request.variant.phone? # true + request.variant.tablet? # false + + request.variant = [:phone, :tablet] + request.variant.phone? # true + request.variant.desktop? # false + request.variant.any?(:phone, :desktop) # true + request.variant.any?(:desktop, :watch) # false + + *George Claghorn* + +* Fix regression where a gzip file response would have a Content-type, + even when it was a 304 status code. + + See #19271. + + *Kohei Suzuki* + +* Fix handling of empty `X_FORWARDED_HOST` header in `raw_host_with_port`. + + Previously, an empty `X_FORWARDED_HOST` header would cause + `Actiondispatch::Http:URL.raw_host_with_port` to return `nil`, causing + `Actiondispatch::Http:URL.host` to raise a `NoMethodError`. + + *Adam Forsyth* + * Drop request class from RouteSet constructor. - - If you would like to use a custom request class, please subclass and implemet + + If you would like to use a custom request class, please subclass and implement the `request_class` method. *tenderlove@ruby-lang.org* diff --git a/actionpack/actionpack.gemspec b/actionpack/actionpack.gemspec index d907001bd6..1bba9df969 100644 --- a/actionpack/actionpack.gemspec +++ b/actionpack/actionpack.gemspec @@ -7,7 +7,7 @@ Gem::Specification.new do |s| s.summary = 'Web-flow and rendering framework putting the VC in MVC (part of Rails).' s.description = 'Web apps on Rails. Simple, battle-tested conventions for building and testing MVC web applications. Works with any Rack-compatible server.' - s.required_ruby_version = '>= 2.2.0' + s.required_ruby_version = '>= 2.2.2' s.license = 'MIT' @@ -23,7 +23,7 @@ Gem::Specification.new do |s| s.add_dependency 'rack', '~> 1.6' s.add_dependency 'rack-test', '~> 0.6.3' - s.add_dependency 'rails-html-sanitizer', '~> 1.0', '>= 1.0.1' + s.add_dependency 'rails-html-sanitizer', '~> 1.0', '>= 1.0.2' s.add_dependency 'rails-dom-testing', '~> 1.0', '>= 1.0.5' s.add_dependency 'actionview', version diff --git a/actionpack/lib/abstract_controller/callbacks.rb b/actionpack/lib/abstract_controller/callbacks.rb index 59ffb0a19e..13795f0dd8 100644 --- a/actionpack/lib/abstract_controller/callbacks.rb +++ b/actionpack/lib/abstract_controller/callbacks.rb @@ -62,9 +62,9 @@ module AbstractController # using #skip_action_callback def skip_action_callback(*names) ActiveSupport::Deprecation.warn('`skip_action_callback` is deprecated and will be removed in the next major version of Rails. Please use skip_before_action, skip_after_action or skip_around_action instead.') - skip_before_action(*names) - skip_after_action(*names) - skip_around_action(*names) + skip_before_action(*names, raise: false) + skip_after_action(*names, raise: false) + skip_around_action(*names, raise: false) end def skip_filter(*names) diff --git a/actionpack/lib/action_controller.rb b/actionpack/lib/action_controller.rb index 7667e469d3..a1893ce920 100644 --- a/actionpack/lib/action_controller.rb +++ b/actionpack/lib/action_controller.rb @@ -12,6 +12,7 @@ module ActionController autoload :Metal autoload :Middleware autoload :Renderer + autoload :FormBuilder autoload_under "metal" do autoload :Compatibility diff --git a/actionpack/lib/action_controller/base.rb b/actionpack/lib/action_controller/base.rb index e6038396f9..bfae372f53 100644 --- a/actionpack/lib/action_controller/base.rb +++ b/actionpack/lib/action_controller/base.rb @@ -221,6 +221,7 @@ module ActionController Cookies, Flash, + FormBuilder, RequestForgeryProtection, ForceSSL, Streaming, diff --git a/actionpack/lib/action_controller/form_builder.rb b/actionpack/lib/action_controller/form_builder.rb new file mode 100644 index 0000000000..f2656ca894 --- /dev/null +++ b/actionpack/lib/action_controller/form_builder.rb @@ -0,0 +1,48 @@ +module ActionController + # Override the default form builder for all views rendered by this + # controller and any of its descendants. Accepts a subclass of + # +ActionView::Helpers::FormBuilder+. + # + # For example, given a form builder: + # + # class AdminFormBuilder < ActionView::Helpers::FormBuilder + # def special_field(name) + # end + # end + # + # The controller specifies a form builder as its default: + # + # class AdminAreaController < ApplicationController + # default_form_builder AdminFormBuilder + # end + # + # Then in the view any form using +form_for+ will be an instance of the + # specified form builder: + # + # <%= form_for(@instance) do |builder| %> + # <%= builder.special_field(:name) %> + # <% end %> + module FormBuilder + extend ActiveSupport::Concern + + included do + class_attribute :_default_form_builder, instance_accessor: false + end + + module ClassMethods + # Set the form builder to be used as the default for all forms + # in the views rendered by this controller and its subclasses. + # + # ==== Parameters + # * <tt>builder</tt> - Default form builder, an instance of +ActionView::Helpers::FormBuilder+ + def default_form_builder(builder) + self._default_form_builder = builder + end + end + + # Default form builder for the controller + def default_form_builder + self.class._default_form_builder + end + end +end diff --git a/actionpack/lib/action_controller/metal/head.rb b/actionpack/lib/action_controller/metal/head.rb index 0d93e2f7aa..70f42bf565 100644 --- a/actionpack/lib/action_controller/metal/head.rb +++ b/actionpack/lib/action_controller/metal/head.rb @@ -38,6 +38,8 @@ module ActionController headers.delete('Content-Type') headers.delete('Content-Length') end + + true end private diff --git a/actionpack/lib/action_controller/metal/http_authentication.rb b/actionpack/lib/action_controller/metal/http_authentication.rb index 2273406948..909ed19a49 100644 --- a/actionpack/lib/action_controller/metal/http_authentication.rb +++ b/actionpack/lib/action_controller/metal/http_authentication.rb @@ -118,7 +118,7 @@ module ActionController end def authentication_request(controller, realm) - controller.headers["WWW-Authenticate"] = %(Basic realm="#{realm.gsub(/"/, "")}") + controller.headers["WWW-Authenticate"] = %(Basic realm="#{realm.tr('"'.freeze, "".freeze)}") controller.status = 401 controller.response_body = "HTTP Basic: Access denied.\n" end @@ -499,7 +499,7 @@ module ActionController # # Returns nothing. def authentication_request(controller, realm) - controller.headers["WWW-Authenticate"] = %(Token realm="#{realm.gsub(/"/, "")}") + controller.headers["WWW-Authenticate"] = %(Token realm="#{realm.tr('"'.freeze, "".freeze)}") controller.__send__ :render, :text => "HTTP Token: Access denied.\n", :status => :unauthorized end end diff --git a/actionpack/lib/action_controller/metal/implicit_render.rb b/actionpack/lib/action_controller/metal/implicit_render.rb index ae04b53825..1573ea7099 100644 --- a/actionpack/lib/action_controller/metal/implicit_render.rb +++ b/actionpack/lib/action_controller/metal/implicit_render.rb @@ -7,7 +7,12 @@ module ActionController end def default_render(*args) - render(*args) + if template_exists?(action_name.to_s, _prefixes, variants: request.variant) + render(*args) + else + logger.info "No template found for #{self.class.name}\##{action_name}, rendering head :no_content" if logger + head :no_content + end end def method_for_action(action_name) diff --git a/actionpack/lib/action_controller/metal/live.rb b/actionpack/lib/action_controller/metal/live.rb index 7590fb6843..58150cd9a9 100644 --- a/actionpack/lib/action_controller/metal/live.rb +++ b/actionpack/lib/action_controller/metal/live.rb @@ -102,7 +102,7 @@ module ActionController end end - message = json.gsub(/\n/, "\ndata: ") + message = json.gsub("\n".freeze, "\ndata: ".freeze) @stream.write "data: #{message}\n\n" end end diff --git a/actionpack/lib/action_controller/metal/mime_responds.rb b/actionpack/lib/action_controller/metal/mime_responds.rb index 7dae171215..fab1be3459 100644 --- a/actionpack/lib/action_controller/metal/mime_responds.rb +++ b/actionpack/lib/action_controller/metal/mime_responds.rb @@ -288,16 +288,17 @@ module ActionController #:nodoc: end def variant - if @variant.nil? + if @variant.empty? @variants[:none] || @variants[:any] - elsif (@variants.keys & @variant).any? - @variant.each do |v| - return @variants[v] if @variants.key?(v) - end else - @variants[:any] + @variants[variant_key] end end + + private + def variant_key + @variant.find { |variant| @variants.key?(variant) } || :any + end end end end diff --git a/actionpack/lib/action_controller/metal/request_forgery_protection.rb b/actionpack/lib/action_controller/metal/request_forgery_protection.rb index 367b736035..663a969f72 100644 --- a/actionpack/lib/action_controller/metal/request_forgery_protection.rb +++ b/actionpack/lib/action_controller/metal/request_forgery_protection.rb @@ -13,9 +13,14 @@ module ActionController #:nodoc: # by including a token in the rendered HTML for your application. This token is # stored as a random string in the session, to which an attacker does not have # access. When a request reaches your application, \Rails verifies the received - # token with the token in the session. Only HTML and JavaScript requests are checked, - # so this will not protect your XML API (presumably you'll have a different - # authentication scheme there anyway). + # token with the token in the session. All requests are checked except GET requests + # as these should be idempotent. Keep in mind that all session-oriented requests + # should be CSRF protected, including Javascript and HTML requests. + # + # Since HTML and Javascript requests are typically made from the browser, we + # need to ensure to verify request authenticity for the web browser. We can + # use session-oriented authentication for these types requests, by using + # the `protect_form_forgery` method in our controllers. # # GET requests are not protected since they don't have side effects like writing # to the database and don't leak sensitive information. JavaScript requests are @@ -26,15 +31,20 @@ module ActionController #:nodoc: # Ajax) requests are allowed to make GET requests for JavaScript responses. # # It's important to remember that XML or JSON requests are also affected and if - # you're building an API you'll need something like: + # you're building an API you should change forgery protection method in + # <tt>ApplicationController</tt> (by default: <tt>:exception</tt>): # # class ApplicationController < ActionController::Base # protect_from_forgery unless: -> { request.format.json? } # end # - # CSRF protection is turned on with the <tt>protect_from_forgery</tt> method, - # which checks the token and resets the session if it doesn't match what was expected. - # A call to this method is generated for new \Rails applications by default. + # CSRF protection is turned on with the <tt>protect_from_forgery</tt> method. + # By default <tt>protect_from_forgery</tt> protects your session with + # <tt>:null_session</tt> method, which provides an empty session during request + # + # We may want to disable CSRF protection for APIs since they are typically + # designed to be state-less. That is, the requestion API client will handle + # the session for you instead of Rails. # # The token parameter is named <tt>authenticity_token</tt> by default. The name and # value of this token must be added to every layout that renders forms by including diff --git a/actionpack/lib/action_controller/metal/strong_parameters.rb b/actionpack/lib/action_controller/metal/strong_parameters.rb index f19c4201ba..c98e937423 100644 --- a/actionpack/lib/action_controller/metal/strong_parameters.rb +++ b/actionpack/lib/action_controller/metal/strong_parameters.rb @@ -117,7 +117,7 @@ module ActionController self.always_permitted_parameters = %w( controller action ) def self.const_missing(const_name) - super unless const_name == :NEVER_UNPERMITTED_PARAMS + return super unless const_name == :NEVER_UNPERMITTED_PARAMS ActiveSupport::Deprecation.warn(<<-MSG.squish) `ActionController::Parameters::NEVER_UNPERMITTED_PARAMS` has been deprecated. Use `ActionController::Parameters.always_permitted_parameters` instead. @@ -268,7 +268,7 @@ module ActionController # # params.permit(:name) # - # +:name+ passes it is a key of +params+ whose associated value is of type + # +:name+ passes if it is a key of +params+ whose associated value is of type # +String+, +Symbol+, +NilClass+, +Numeric+, +TrueClass+, +FalseClass+, # +Date+, +Time+, +DateTime+, +StringIO+, +IO+, # +ActionDispatch::Http::UploadedFile+ or +Rack::Test::UploadedFile+. diff --git a/actionpack/lib/action_controller/metal/url_for.rb b/actionpack/lib/action_controller/metal/url_for.rb index fbaa90d521..5a0e5c62e4 100644 --- a/actionpack/lib/action_controller/metal/url_for.rb +++ b/actionpack/lib/action_controller/metal/url_for.rb @@ -5,9 +5,9 @@ module ActionController # In addition to <tt>AbstractController::UrlFor</tt>, this module accesses the HTTP layer to define # url options like the +host+. In order to do so, this module requires the host class # to implement +env+ which needs to be Rack-compatible and +request+ - # which is either instance of +ActionDispatch::Request+ or an object - # that responds to <tt>host</tt>, <tt>optional_port</tt>, <tt>protocol</tt> and - # <tt>symbolized_path_parameter</tt> methods. + # which is either an instance of +ActionDispatch::Request+ or an object + # that responds to the +host+, +optional_port+, +protocol+ and + # +symbolized_path_parameter+ methods. # # class RootUrl # include ActionController::UrlFor diff --git a/actionpack/lib/action_controller/test_case.rb b/actionpack/lib/action_controller/test_case.rb index 4782991463..33c24999f9 100644 --- a/actionpack/lib/action_controller/test_case.rb +++ b/actionpack/lib/action_controller/test_case.rb @@ -201,7 +201,7 @@ module ActionController super self.session = TestSession.new - self.session_options = TestSession::DEFAULT_OPTIONS.merge(:id => SecureRandom.hex(16)) + self.session_options = TestSession::DEFAULT_OPTIONS end def assign_parameters(routes, controller_path, action, parameters = {}) diff --git a/actionpack/lib/action_dispatch/http/mime_negotiation.rb b/actionpack/lib/action_dispatch/http/mime_negotiation.rb index 53a98c5d0a..ff336b7354 100644 --- a/actionpack/lib/action_dispatch/http/mime_negotiation.rb +++ b/actionpack/lib/action_dispatch/http/mime_negotiation.rb @@ -10,8 +10,6 @@ module ActionDispatch self.ignore_accept_header = false end - attr_reader :variant - # The MIME type of the HTTP request, such as Mime::XML. # # For backward compatibility, the post \format is extracted from the @@ -75,18 +73,22 @@ module ActionDispatch # Sets the \variant for template. def variant=(variant) - if variant.is_a?(Symbol) - @variant = [variant] - elsif variant.nil? || variant.is_a?(Array) && variant.any? && variant.all?{ |v| v.is_a?(Symbol) } - @variant = variant + variant = Array(variant) + + if variant.all? { |v| v.is_a?(Symbol) } + @variant = ActiveSupport::ArrayInquirer.new(variant) else - raise ArgumentError, "request.variant must be set to a Symbol or an Array of Symbols, not a #{variant.class}. " \ + raise ArgumentError, "request.variant must be set to a Symbol or an Array of Symbols. " \ "For security reasons, never directly set the variant to a user-provided value, " \ "like params[:variant].to_sym. Check user-provided value against a whitelist first, " \ "then set the variant: request.variant = :tablet if params[:variant] == 'tablet'" end end + def variant + @variant ||= ActiveSupport::ArrayInquirer.new + end + # Sets the \format by string extension, which can be used to force custom formats # that are not controlled by the extension. # diff --git a/actionpack/lib/action_dispatch/http/request.rb b/actionpack/lib/action_dispatch/http/request.rb index 732ee67268..a1f84e5ace 100644 --- a/actionpack/lib/action_dispatch/http/request.rb +++ b/actionpack/lib/action_dispatch/http/request.rb @@ -340,7 +340,7 @@ module ActionDispatch end protected - def parse_query(qs) + def parse_query(*) Utils.deep_munge(super) end diff --git a/actionpack/lib/action_dispatch/http/url.rb b/actionpack/lib/action_dispatch/http/url.rb index 7da6301ac4..f5b709ccd6 100644 --- a/actionpack/lib/action_dispatch/http/url.rb +++ b/actionpack/lib/action_dispatch/http/url.rb @@ -229,7 +229,7 @@ module ActionDispatch # req = Request.new 'HTTP_HOST' => 'example.com:8080' # req.raw_host_with_port # => "example.com:8080" def raw_host_with_port - if forwarded = env["HTTP_X_FORWARDED_HOST"] + if forwarded = env["HTTP_X_FORWARDED_HOST"].presence forwarded.split(/,\s?/).last else env['HTTP_HOST'] || "#{env['SERVER_NAME'] || env['SERVER_ADDR']}:#{env['SERVER_PORT']}" diff --git a/actionpack/lib/action_dispatch/journey/formatter.rb b/actionpack/lib/action_dispatch/journey/formatter.rb index 992c1a9efe..c0566c6fc9 100644 --- a/actionpack/lib/action_dispatch/journey/formatter.rb +++ b/actionpack/lib/action_dispatch/journey/formatter.rb @@ -39,7 +39,7 @@ module ActionDispatch return [route.format(parameterized_parts), params] end - message = "No route matches #{Hash[constraints.sort].inspect}" + message = "No route matches #{Hash[constraints.sort_by{|k,v| k.to_s}].inspect}" message << " missing required keys: #{missing_keys.sort.inspect}" unless missing_keys.empty? raise ActionController::UrlGenerationError, message diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb index b7687ca100..139706ecb9 100644 --- a/actionpack/lib/action_dispatch/middleware/cookies.rb +++ b/actionpack/lib/action_dispatch/middleware/cookies.rb @@ -181,7 +181,7 @@ module ActionDispatch # 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 + module VerifyAndUpgradeLegacySignedMessage # :nodoc: def initialize(*args) super @legacy_verifier = ActiveSupport::MessageVerifier.new(@options[:secret_token], serializer: ActiveSupport::MessageEncryptor::NullSerializer) @@ -392,7 +392,7 @@ module ActionDispatch end end - class JsonSerializer + class JsonSerializer # :nodoc: def self.load(value) ActiveSupport::JSON.decode(value) end @@ -402,7 +402,7 @@ module ActionDispatch end end - module SerializedCookieJars + module SerializedCookieJars # :nodoc: MARSHAL_SIGNATURE = "\x04\x08".freeze protected @@ -454,12 +454,16 @@ module ActionDispatch @verifier = ActiveSupport::MessageVerifier.new(secret, digest: digest, serializer: ActiveSupport::MessageEncryptor::NullSerializer) end + # Returns the value of the cookie by +name+ if it is untampered, + # returns +nil+ otherwise or if no such cookie exists. def [](name) if signed_message = @parent_jar[name] deserialize name, verify(signed_message) end end + # Signs and 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! @@ -482,8 +486,8 @@ module ActionDispatch # UpgradeLegacySignedCookieJar is used instead of SignedCookieJar if # secrets.secret_token and secrets.secret_key_base are both set. It reads - # legacy cookies signed with the old dummy key generator and re-saves - # them using the new key generator to provide a smooth upgrade path. + # legacy cookies signed with the old dummy key generator and signs and + # re-saves them using the new key generator to provide a smooth upgrade path. class UpgradeLegacySignedCookieJar < SignedCookieJar #:nodoc: include VerifyAndUpgradeLegacySignedMessage @@ -511,12 +515,16 @@ module ActionDispatch @encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret, digest: digest, serializer: ActiveSupport::MessageEncryptor::NullSerializer) end + # Returns the value of the cookie by +name+ if it is untampered, + # returns +nil+ otherwise or if no such cookie exists. def [](name) if encrypted_message = @parent_jar[name] deserialize name, decrypt_and_verify(encrypted_message) end end + # Encrypts and 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! diff --git a/actionpack/lib/action_dispatch/middleware/public_exceptions.rb b/actionpack/lib/action_dispatch/middleware/public_exceptions.rb index 040cb215b7..7cde76b30e 100644 --- a/actionpack/lib/action_dispatch/middleware/public_exceptions.rb +++ b/actionpack/lib/action_dispatch/middleware/public_exceptions.rb @@ -17,10 +17,10 @@ module ActionDispatch end def call(env) - status = env["PATH_INFO"][1..-1] + status = env["PATH_INFO"][1..-1].to_i request = ActionDispatch::Request.new(env) content_type = request.formats.first - body = { :status => status, :error => Rack::Utils::HTTP_STATUS_CODES.fetch(status.to_i, Rack::Utils::HTTP_STATUS_CODES[500]) } + body = { :status => status, :error => Rack::Utils::HTTP_STATUS_CODES.fetch(status, Rack::Utils::HTTP_STATUS_CODES[500]) } render(status, content_type, body) end diff --git a/actionpack/lib/action_dispatch/middleware/static.rb b/actionpack/lib/action_dispatch/middleware/static.rb index 2e1bd45c3d..c47e5d5245 100644 --- a/actionpack/lib/action_dispatch/middleware/static.rb +++ b/actionpack/lib/action_dispatch/middleware/static.rb @@ -3,15 +3,15 @@ require 'active_support/core_ext/uri' module ActionDispatch # This middleware returns a file's contents from disk in the body response. - # When initialized it can accept an optional 'Cache-Control' header which + # When initialized, it can accept an optional 'Cache-Control' header, which # will be set when a response containing a file's contents is delivered. # # This middleware will render the file specified in `env["PATH_INFO"]` - # where the base path is in the +root+ directory. For example if the +root+ - # is set to `public/` then a request with `env["PATH_INFO"]` of - # `assets/application.js` will return a response with contents of a file + # where the base path is in the +root+ directory. For example, if the +root+ + # is set to `public/`, then a request with `env["PATH_INFO"]` of + # `assets/application.js` will return a response with the contents of a file # located at `public/assets/application.js` if the file exists. If the file - # does not exist a 404 "File not Found" response will be returned. + # does not exist, a 404 "File not Found" response will be returned. class FileHandler def initialize(root, cache_control) @root = root.chomp('/') @@ -20,6 +20,13 @@ module ActionDispatch @file_server = ::Rack::File.new(@root, headers) end + + # Takes a path to a file. If the file is found, has valid encoding, and has + # correct read permissions, the return value is a URI-escaped string + # representing the filename. Otherwise, false is returned. + # + # Used by the `Static` class to check the existence of a valid file + # in the server's `public/` directory. (See Static#call) def match?(path) path = URI.parser.unescape(path) return false unless path.valid_encoding? @@ -28,7 +35,7 @@ module ActionDispatch paths = [path, "#{path}#{ext}", "#{path}/index#{ext}"] if match = paths.detect { |p| - path = File.join(@root, p) + path = File.join(@root, p.force_encoding('UTF-8')) begin File.file?(path) && File.readable?(path) rescue SystemCallError @@ -47,6 +54,9 @@ module ActionDispatch if gzip_path && gzip_encoding_accepted?(env) env['PATH_INFO'] = gzip_path status, headers, body = @file_server.call(env) + if status == 304 + return [status, headers, body] + end headers['Content-Encoding'] = 'gzip' headers['Content-Type'] = content_type(path) else @@ -85,7 +95,7 @@ module ActionDispatch end # This middleware will attempt to return the contents of a file's body from - # disk in the response. If a file is not found on disk, the request will be + # disk in the response. If a file is not found on disk, the request will be # delegated to the application stack. This middleware is commonly initialized # to serve assets from a server's `public/` directory. # diff --git a/actionpack/lib/action_dispatch/request/session.rb b/actionpack/lib/action_dispatch/request/session.rb index 973627f106..9a1a05e971 100644 --- a/actionpack/lib/action_dispatch/request/session.rb +++ b/actionpack/lib/action_dispatch/request/session.rb @@ -9,7 +9,8 @@ module ActionDispatch # Singleton object used to determine if an optional param wasn't specified Unspecified = Object.new - + + # Creates a session hash, merging the properties of the previous session if any def self.create(store, env, default_options) session_was = find env session = Request::Session.new(store, env) diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb index 34b5b48f3a..49009a45cc 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -1678,7 +1678,7 @@ module ActionDispatch end def shallow_nesting_depth #:nodoc: - @nesting.select(&:shallow?).size + @nesting.count(&:shallow?) end def param_constraint? #:nodoc: diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb index 0f3734dd74..d0d8ded515 100644 --- a/actionpack/lib/action_dispatch/routing/route_set.rb +++ b/actionpack/lib/action_dispatch/routing/route_set.rb @@ -226,7 +226,7 @@ module ActionDispatch params = parameterize_args(args) { |missing_key| missing_keys << missing_key } - constraints = Hash[@route.requirements.merge(params).sort] + constraints = Hash[@route.requirements.merge(params).sort_by{|k,v| k.to_s}] message = "No route matches #{constraints.inspect}" message << " missing required keys: #{missing_keys.sort.inspect}" diff --git a/actionpack/lib/action_dispatch/testing/assertions.rb b/actionpack/lib/action_dispatch/testing/assertions.rb index f325c35b57..21b3b89d22 100644 --- a/actionpack/lib/action_dispatch/testing/assertions.rb +++ b/actionpack/lib/action_dispatch/testing/assertions.rb @@ -12,7 +12,7 @@ module ActionDispatch include Rails::Dom::Testing::Assertions def html_document - @html_document ||= if @response.content_type =~ /xml$/ + @html_document ||= if @response.content_type === Mime::XML Nokogiri::XML::Document.parse(@response.body) else Nokogiri::HTML::Document.parse(@response.body) diff --git a/actionpack/lib/action_dispatch/testing/integration.rb b/actionpack/lib/action_dispatch/testing/integration.rb index f7f898288b..3800c61dab 100644 --- a/actionpack/lib/action_dispatch/testing/integration.rb +++ b/actionpack/lib/action_dispatch/testing/integration.rb @@ -2,6 +2,7 @@ require 'stringio' require 'uri' require 'active_support/core_ext/kernel/singleton_class' require 'active_support/core_ext/object/try' +require 'active_support/core_ext/string/strip' require 'rack/test' require 'minitest' @@ -388,8 +389,16 @@ module ActionDispatch APP_SESSIONS = {} - def app - @app ||= nil + attr_reader :app + + def before_setup + @app = nil + @integration_session = nil + super + end + + def integration_session + @integration_session ||= create_session(app) end # Reset the current session. This is useful for testing multiple sessions @@ -417,8 +426,6 @@ module ActionDispatch %w(get post patch put head delete cookies assigns xml_http_request xhr get_via_redirect post_via_redirect).each do |method| define_method(method) do |*args| - reset! unless integration_session - # reset the html_document variable, except for cookies/assigns calls unless method == 'cookies' || method == 'assigns' @html_document = nil @@ -450,19 +457,16 @@ module ActionDispatch # Copy the instance variables from the current session instance into the # test instance. def copy_session_variables! #:nodoc: - return unless integration_session @controller = @integration_session.controller @response = @integration_session.response @request = @integration_session.request end def default_url_options - reset! unless integration_session integration_session.default_url_options end def default_url_options=(options) - reset! unless integration_session integration_session.default_url_options = options end @@ -472,7 +476,6 @@ module ActionDispatch # Delegate unhandled messages to the current session instance. def method_missing(sym, *args, &block) - reset! unless integration_session if integration_session.respond_to?(sym) integration_session.__send__(sym, *args, &block).tap do copy_session_variables! @@ -481,11 +484,6 @@ module ActionDispatch super end end - - private - def integration_session - @integration_session ||= nil - end end end @@ -508,8 +506,8 @@ module ActionDispatch # assert_equal 200, status # # # post the login and follow through to the home page - # post "/login", username: people(:jamis).username, - # password: people(:jamis).password + # post "/login", params: { username: people(:jamis).username, + # password: people(:jamis).password } # follow_redirect! # assert_equal 200, status # assert_equal "/home", path @@ -548,7 +546,7 @@ module ActionDispatch # end # # def speak(room, message) - # xml_http_request "/say/#{room.id}", message: message + # post "/say/#{room.id}", xhr: true, params: { message: message } # assert(...) # ... # end @@ -558,8 +556,8 @@ module ActionDispatch # open_session do |sess| # sess.extend(CustomAssertions) # who = people(who) - # sess.post "/login", username: who.username, - # password: who.password + # sess.post "/login", params: { username: who.username, + # password: who.password } # assert(...) # end # end @@ -578,7 +576,8 @@ module ActionDispatch # get "/login" # assert_response :success # - # post_via_redirect "/login", username: users(:david).username, password: users(:david).password + # post "/login", params: { username: users(:david).username, password: users(:david).password } + # follow_redirect! # assert_equal '/welcome', path # assert_equal 'Welcome david!', flash[:notice] # @@ -633,7 +632,7 @@ module ActionDispatch # sess.extend(CustomDsl) # u = users(user) # sess.https! - # sess.post "/login", username: u.username, password: u.password + # sess.post "/login", params: { username: u.username, password: u.password } # assert_equal '/welcome', sess.path # sess.https!(false) # end @@ -662,7 +661,6 @@ module ActionDispatch end def url_options - reset! unless integration_session integration_session.url_options end diff --git a/actionpack/test/abstract_unit.rb b/actionpack/test/abstract_unit.rb index 62ff1be5c9..c1be2c9afe 100644 --- a/actionpack/test/abstract_unit.rb +++ b/actionpack/test/abstract_unit.rb @@ -14,7 +14,11 @@ silence_warnings do end require 'drb' -require 'drb/unix' +begin + require 'drb/unix' +rescue LoadError + puts "'drb/unix' is not available" +end require 'tempfile' PROCESS_COUNT = (ENV['N'] || 4).to_i diff --git a/actionpack/test/controller/force_ssl_test.rb b/actionpack/test/controller/force_ssl_test.rb index 04222745d9..5639abdc56 100644 --- a/actionpack/test/controller/force_ssl_test.rb +++ b/actionpack/test/controller/force_ssl_test.rb @@ -315,7 +315,7 @@ class RedirectToSSLTest < ActionController::TestCase assert_equal "https://secure.cheeseburger.host/redirect_to_ssl/cheeseburger", redirect_to_url end - def test_banana_does_not_redirect_if_already_https + def test_cheeseburgers_does_not_redirect_if_already_https request.env['HTTPS'] = 'on' get :cheeseburger assert_response 200 diff --git a/actionpack/test/controller/form_builder_test.rb b/actionpack/test/controller/form_builder_test.rb new file mode 100644 index 0000000000..99eeaf9ab6 --- /dev/null +++ b/actionpack/test/controller/form_builder_test.rb @@ -0,0 +1,17 @@ +require 'abstract_unit' + +class FormBuilderController < ActionController::Base + class SpecializedFormBuilder < ActionView::Helpers::FormBuilder ; end + + default_form_builder SpecializedFormBuilder +end + +class ControllerFormBuilderTest < ActiveSupport::TestCase + setup do + @controller = FormBuilderController.new + end + + def test_default_form_builder_assigned + assert_equal FormBuilderController::SpecializedFormBuilder, @controller.default_form_builder + end +end diff --git a/actionpack/test/controller/integration_test.rb b/actionpack/test/controller/integration_test.rb index 438c044da2..a87059bee4 100644 --- a/actionpack/test/controller/integration_test.rb +++ b/actionpack/test/controller/integration_test.rb @@ -363,6 +363,7 @@ class IntegrationProcessTest < ActionDispatch::IntegrationTest respond_to do |format| format.html { render :text => "OK", :status => 200 } format.js { render :text => "JS OK", :status => 200 } + format.xml { render :xml => "<root></root>", :status => 200 } end end @@ -419,6 +420,22 @@ class IntegrationProcessTest < ActionDispatch::IntegrationTest end end + def test_get_xml + with_test_route_set do + get "/get", params: {}, headers: {"HTTP_ACCEPT" => "application/xml"} + assert_equal 200, status + assert_equal "OK", status_message + assert_response 200 + assert_response :success + assert_response :ok + assert_equal({}, cookies.to_hash) + assert_equal "<root></root>", body + assert_equal "<root></root>", response.body + assert_instance_of Nokogiri::XML::Document, html_document + assert_equal 1, request_count + end + end + def test_post with_test_route_set do post '/post' @@ -1036,3 +1053,15 @@ class IntegrationRequestsWithoutSetup < ActionDispatch::IntegrationTest end end end + +# to ensure that session requirements in setup are persisted in the tests +class IntegrationRequestsWithSessionSetup < ActionDispatch::IntegrationTest + setup do + cookies['user_name'] = 'david' + end + + def test_cookies_set_in_setup_are_persisted_through_the_session + get "/foo" + assert_equal({"user_name"=>"david"}, cookies.to_hash) + end +end diff --git a/actionpack/test/controller/mime/respond_to_test.rb b/actionpack/test/controller/mime/respond_to_test.rb index 1f5f66dc80..7aef8a50ce 100644 --- a/actionpack/test/controller/mime/respond_to_test.rb +++ b/actionpack/test/controller/mime/respond_to_test.rb @@ -1,4 +1,5 @@ require 'abstract_unit' +require "active_support/log_subscriber/test_helper" class RespondToController < ActionController::Base layout :set_layout @@ -608,19 +609,29 @@ class RespondToControllerTest < ActionController::TestCase end def test_invalid_variant + logger = ActiveSupport::LogSubscriber::TestHelper::MockLogger.new + old_logger, ActionController::Base.logger = ActionController::Base.logger, logger + @request.variant = :invalid - assert_raises(ActionView::MissingTemplate) do - get :variant_with_implicit_rendering - end + get :variant_with_implicit_rendering + assert_response :no_content + assert_equal 1, logger.logged(:info).select{ |s| s =~ /No template found/ }.size, "Implicit head :no_content not logged" + ensure + ActionController::Base.logger = old_logger end def test_variant_not_set_regular_template_missing - assert_raises(ActionView::MissingTemplate) do - get :variant_with_implicit_rendering - end + get :variant_with_implicit_rendering + assert_response :no_content end def test_variant_with_implicit_rendering + @request.variant = :implicit + get :variant_with_implicit_rendering + assert_response :no_content + end + + def test_variant_with_implicit_template_rendering @request.variant = :mobile get :variant_with_implicit_rendering assert_equal "text/html", @response.content_type diff --git a/actionpack/test/controller/new_base/render_template_test.rb b/actionpack/test/controller/new_base/render_template_test.rb index 19fef718e7..b06ce5db40 100644 --- a/actionpack/test/controller/new_base/render_template_test.rb +++ b/actionpack/test/controller/new_base/render_template_test.rb @@ -186,21 +186,21 @@ module RenderTemplate end end - test "rendering with layout => :true" do + test "rendering with layout => true" do get "/render_template/with_layout/with_layout" assert_body "Hello from basic.html.erb, I'm here!" assert_status 200 end - test "rendering with layout => :false" do + test "rendering with layout => false" do get "/render_template/with_layout/with_layout_false" assert_body "Hello from basic.html.erb" assert_status 200 end - test "rendering with layout => :nil" do + test "rendering with layout => nil" do get "/render_template/with_layout/with_layout_nil" assert_body "Hello from basic.html.erb" diff --git a/actionpack/test/controller/parameters/always_permitted_parameters_test.rb b/actionpack/test/controller/parameters/always_permitted_parameters_test.rb index 059f310d49..59be08db54 100644 --- a/actionpack/test/controller/parameters/always_permitted_parameters_test.rb +++ b/actionpack/test/controller/parameters/always_permitted_parameters_test.rb @@ -1,5 +1,6 @@ require 'abstract_unit' require 'action_controller/metal/strong_parameters' +require 'minitest/mock' class AlwaysPermittedParametersTest < ActiveSupport::TestCase def setup @@ -14,7 +15,13 @@ class AlwaysPermittedParametersTest < ActiveSupport::TestCase test "shows deprecations warning on NEVER_UNPERMITTED_PARAMS" do assert_deprecated do - ActionController::Parameters::NEVER_UNPERMITTED_PARAMS + ActionController::Parameters::NEVER_UNPERMITTED_PARAMS + end + end + + test "returns super on missing constant other than NEVER_UNPERMITTED_PARAMS" do + ActionController::Parameters.superclass.stub :const_missing, "super" do + assert_equal "super", ActionController::Parameters::NON_EXISTING_CONSTANT end end diff --git a/actionpack/test/controller/redirect_test.rb b/actionpack/test/controller/redirect_test.rb index 103ca9c776..ef30f1ea0f 100644 --- a/actionpack/test/controller/redirect_test.rb +++ b/actionpack/test/controller/redirect_test.rb @@ -1,8 +1,5 @@ require 'abstract_unit' -class WorkshopsController < ActionController::Base -end - class RedirectController < ActionController::Base # empty method not used anywhere to ensure methods like # `status` and `location` aren't called on `redirect_to` calls @@ -63,7 +60,7 @@ class RedirectController < ActionController::Base end def redirect_to_url_with_unescaped_query_string - redirect_to "http://dev.rubyonrails.org/query?status=new" + redirect_to "http://example.com/query?status=new" end def redirect_to_url_with_complex_scheme @@ -233,7 +230,7 @@ class RedirectTest < ActionController::TestCase def test_redirect_to_url_with_unescaped_query_string get :redirect_to_url_with_unescaped_query_string assert_response :redirect - assert_redirected_to "http://dev.rubyonrails.org/query?status=new" + assert_redirected_to "http://example.com/query?status=new" end def test_redirect_to_url_with_complex_scheme diff --git a/actionpack/test/controller/render_test.rb b/actionpack/test/controller/render_test.rb index 488585c7a4..79e2104789 100644 --- a/actionpack/test/controller/render_test.rb +++ b/actionpack/test/controller/render_test.rb @@ -173,6 +173,11 @@ class TestController < ActionController::Base head :forbidden, :x_custom_header => "something" end + def head_and_return + head :ok and return + raise 'should not reach this line' + end + def head_with_no_content # Fill in the headers with dummy data to make # sure they get removed during the testing @@ -560,6 +565,12 @@ class HeadRenderTest < ActionController::TestCase assert_equal "something", @response.headers["X-Custom-Header"] assert_response :forbidden end + + def test_head_returns_truthy_value + assert_nothing_raised do + get :head_and_return + end + end end class HttpCacheForeverTest < ActionController::TestCase diff --git a/actionpack/test/controller/request/test_request_test.rb b/actionpack/test/controller/request/test_request_test.rb index e624f11773..77a2f68b1c 100644 --- a/actionpack/test/controller/request/test_request_test.rb +++ b/actionpack/test/controller/request/test_request_test.rb @@ -24,12 +24,4 @@ class ActionController::TestRequestTest < ActiveSupport::TestCase end end - def test_session_id_exists_by_default - assert_not_nil(@request.session_options[:id]) - end - - def test_session_id_different_on_each_call - assert_not_equal(@request.session_options[:id], ActionController::TestRequest.new.session_options[:id]) - end - end diff --git a/actionpack/test/controller/show_exceptions_test.rb b/actionpack/test/controller/show_exceptions_test.rb index fba5ebba15..786dc15444 100644 --- a/actionpack/test/controller/show_exceptions_test.rb +++ b/actionpack/test/controller/show_exceptions_test.rb @@ -75,7 +75,7 @@ module ShowExceptions get "/", headers: { 'HTTP_ACCEPT' => 'application/json' } assert_response :internal_server_error assert_equal 'application/json', response.content_type.to_s - assert_equal({ :status => '500', :error => 'Internal Server Error' }.to_json, response.body) + assert_equal({ :status => 500, :error => 'Internal Server Error' }.to_json, response.body) end def test_render_xml_exception @@ -83,7 +83,7 @@ module ShowExceptions get "/", headers: { 'HTTP_ACCEPT' => 'application/xml' } assert_response :internal_server_error assert_equal 'application/xml', response.content_type.to_s - assert_equal({ :status => '500', :error => 'Internal Server Error' }.to_xml, response.body) + assert_equal({ :status => 500, :error => 'Internal Server Error' }.to_xml, response.body) end def test_render_fallback_exception diff --git a/actionpack/test/dispatch/request_test.rb b/actionpack/test/dispatch/request_test.rb index ee8e915610..f208cfda89 100644 --- a/actionpack/test/dispatch/request_test.rb +++ b/actionpack/test/dispatch/request_test.rb @@ -435,6 +435,9 @@ class RequestHost < BaseRequestTest request = stub_request 'HTTP_X_FORWARDED_HOST' => "www.firsthost.org, www.secondhost.org" assert_equal "www.secondhost.org", request.host + + request = stub_request 'HTTP_X_FORWARDED_HOST' => "", 'HTTP_HOST' => "rubyonrails.org" + assert_equal "rubyonrails.org", request.host end test "http host with default port overrides server port" do @@ -1125,35 +1128,47 @@ class RequestEtag < BaseRequestTest end class RequestVariant < BaseRequestTest - test "setting variant" do - request = stub_request + def setup + super + @request = stub_request + end - request.variant = :mobile - assert_equal [:mobile], request.variant + test 'setting variant to a symbol' do + @request.variant = :phone - request.variant = [:phone, :tablet] - assert_equal [:phone, :tablet], request.variant + assert @request.variant.phone? + assert_not @request.variant.tablet? + assert @request.variant.any?(:phone, :tablet) + assert_not @request.variant.any?(:tablet, :desktop) + end - assert_raise ArgumentError do - request.variant = [:phone, "tablet"] - end + test 'setting variant to an array of symbols' do + @request.variant = [:phone, :tablet] - assert_raise ArgumentError do - request.variant = "yolo" - end + assert @request.variant.phone? + assert @request.variant.tablet? + assert_not @request.variant.desktop? + assert @request.variant.any?(:tablet, :desktop) + assert_not @request.variant.any?(:desktop, :watch) end - test "reset variant" do - request = stub_request + test 'clearing variant' do + @request.variant = nil - request.variant = nil - assert_equal nil, request.variant + assert @request.variant.empty? + assert_not @request.variant.phone? + assert_not @request.variant.any?(:phone, :tablet) end - test "setting variant with non symbol value" do - request = stub_request + test 'setting variant to a non-symbol value' do + assert_raise ArgumentError do + @request.variant = 'phone' + end + end + + test 'setting variant to an array containing a non-symbol value' do assert_raise ArgumentError do - request.variant = "mobile" + @request.variant = [:phone, 'tablet'] end end end diff --git a/actionpack/test/dispatch/response_test.rb b/actionpack/test/dispatch/response_test.rb index c61423dce4..5fbd19acdf 100644 --- a/actionpack/test/dispatch/response_test.rb +++ b/actionpack/test/dispatch/response_test.rb @@ -254,10 +254,6 @@ class ResponseTest < ActiveSupport::TestCase end class ResponseIntegrationTest < ActionDispatch::IntegrationTest - def app - @app - end - test "response cache control from railsish app" do @app = lambda { |env| ActionDispatch::Response.new.tap { |resp| diff --git a/actionpack/test/dispatch/routing_test.rb b/actionpack/test/dispatch/routing_test.rb index 55fc160ac8..62c99a2edc 100644 --- a/actionpack/test/dispatch/routing_test.rb +++ b/actionpack/test/dispatch/routing_test.rb @@ -4476,6 +4476,19 @@ class TestUrlGenerationErrors < ActionDispatch::IntegrationTest error = assert_raises(ActionController::UrlGenerationError, message){ product_path(id: nil) } assert_equal message, error.message end + + test "url helpers raise message with mixed parameters when generation fails " do + url, missing = { action: 'show', controller: 'products', id: nil, "id"=>"url-tested"}, [:id] + message = "No route matches #{url.inspect} missing required keys: #{missing.inspect}" + + # Optimized url helper + error = assert_raises(ActionController::UrlGenerationError){ product_path(nil, 'id'=>'url-tested') } + assert_equal message, error.message + + # Non-optimized url helper + error = assert_raises(ActionController::UrlGenerationError, message){ product_path(id: nil, 'id'=>'url-tested') } + assert_equal message, error.message + end end class TestDefaultUrlOptions < ActionDispatch::IntegrationTest diff --git a/actionpack/test/dispatch/session/cache_store_test.rb b/actionpack/test/dispatch/session/cache_store_test.rb index 9f810cad01..22a46b0930 100644 --- a/actionpack/test/dispatch/session/cache_store_test.rb +++ b/actionpack/test/dispatch/session/cache_store_test.rb @@ -22,7 +22,7 @@ class CacheStoreTest < ActionDispatch::IntegrationTest end def get_session_id - render :text => "#{request.session_options[:id]}" + render :text => "#{request.session.id}" end def call_reset_session diff --git a/actionpack/test/dispatch/session/cookie_store_test.rb b/actionpack/test/dispatch/session/cookie_store_test.rb index 2194efa503..e7f4235de8 100644 --- a/actionpack/test/dispatch/session/cookie_store_test.rb +++ b/actionpack/test/dispatch/session/cookie_store_test.rb @@ -29,7 +29,7 @@ class CookieStoreTest < ActionDispatch::IntegrationTest end def get_session_id - render :text => "id: #{request.session_options[:id]}" + render :text => "id: #{request.session.id}" end def get_class_after_reset_session @@ -53,7 +53,7 @@ class CookieStoreTest < ActionDispatch::IntegrationTest end def change_session_id - request.session_options[:id] = nil + request.session.options[:id] = nil get_session_id end diff --git a/actionpack/test/dispatch/session/mem_cache_store_test.rb b/actionpack/test/dispatch/session/mem_cache_store_test.rb index fbd82945cc..9a5d5131c0 100644 --- a/actionpack/test/dispatch/session/mem_cache_store_test.rb +++ b/actionpack/test/dispatch/session/mem_cache_store_test.rb @@ -23,7 +23,7 @@ class MemCacheStoreTest < ActionDispatch::IntegrationTest end def get_session_id - render :text => "#{request.session_options[:id]}" + render :text => "#{request.session.id}" end def call_reset_session diff --git a/actionpack/test/dispatch/static_test.rb b/actionpack/test/dispatch/static_test.rb index ebc9d71403..93e5c85a97 100644 --- a/actionpack/test/dispatch/static_test.rb +++ b/actionpack/test/dispatch/static_test.rb @@ -2,6 +2,20 @@ require 'abstract_unit' require 'zlib' module StaticTests + def setup + silence_warnings do + @default_internal_encoding = Encoding.default_internal + @default_external_encoding = Encoding.default_external + end + end + + def teardown + silence_warnings do + Encoding.default_internal = @default_internal_encoding + Encoding.default_external = @default_external_encoding + end + end + def test_serves_dynamic_content assert_equal "Hello, World!", get("/nofile").body end @@ -10,6 +24,18 @@ module StaticTests assert_equal "Hello, World!", get("/doorkeeper%E3E4").body end + def test_handles_urls_with_ascii_8bit + assert_equal "Hello, World!", get("/doorkeeper%E3E4".force_encoding('ASCII-8BIT')).body + end + + def test_handles_urls_with_ascii_8bit_on_win_31j + silence_warnings do + Encoding.default_internal = "Windows-31J" + Encoding.default_external = "Windows-31J" + end + assert_equal "Hello, World!", get("/doorkeeper%E3E4".force_encoding('ASCII-8BIT')).body + end + def test_sets_cache_control response = get("/index.html") assert_html "/index.html", response @@ -143,6 +169,16 @@ module StaticTests assert_equal default_response.headers['Content-Type'], response.headers['Content-Type'] end + def test_serves_gzip_files_with_not_modified + file_name = "/gzip/application-a71b3024f80aea3181c09774ca17e712.js" + last_modified = File.mtime(File.join(@root, "#{file_name}.gz")) + response = get(file_name, 'HTTP_ACCEPT_ENCODING' => 'gzip', 'HTTP_IF_MODIFIED_SINCE' => last_modified.httpdate) + assert_equal 304, response.status + assert_equal nil, response.headers['Content-Type'] + assert_equal nil, response.headers['Content-Encoding'] + assert_equal nil, response.headers['Vary'] + end + # Windows doesn't allow \ / : * ? " < > | in filenames unless RbConfig::CONFIG['host_os'] =~ /mswin|mingw/ def test_serves_static_file_with_colon @@ -198,6 +234,7 @@ class StaticTest < ActiveSupport::TestCase } def setup + super @root = "#{FIXTURE_LOAD_PATH}/public" @app = ActionDispatch::Static.new(DummyApp, @root, "public, max-age=60") end @@ -227,6 +264,7 @@ end class StaticEncodingTest < StaticTest def setup + super @root = "#{FIXTURE_LOAD_PATH}/公共" @app = ActionDispatch::Static.new(DummyApp, @root, "public, max-age=60") end diff --git a/actionpack/test/journey/router_test.rb b/actionpack/test/journey/router_test.rb index 19c61b5914..a134e343cc 100644 --- a/actionpack/test/journey/router_test.rb +++ b/actionpack/test/journey/router_test.rb @@ -401,6 +401,33 @@ module ActionDispatch assert_equal({:id => 1, :relative_url_root => nil}, params) end + def test_generate_missing_keys_no_matches_different_format_keys + path = Path::Pattern.from_string '/:controller/:action/:name' + @router.routes.add_route @app, path, {}, {}, {} + primarty_parameters = { + :id => 1, + :controller => "tasks", + :action => "show", + :relative_url_root => nil + } + redirection_parameters = { + 'action'=>'show', + } + missing_key = 'name' + missing_parameters ={ + missing_key => "task_1" + } + request_parameters = primarty_parameters.merge(redirection_parameters).merge(missing_parameters) + + message = "No route matches #{Hash[request_parameters.sort_by{|k,v|k.to_s}].inspect} missing required keys: #{[missing_key.to_sym].inspect}" + + error = assert_raises(ActionController::UrlGenerationError) do + @formatter.generate( + nil, request_parameters, request_parameters) + end + assert_equal message, error.message + end + def test_generate_uses_recall_if_needed path = Path::Pattern.from_string '/:controller(/:action(/:id))' @router.routes.add_route @app, path, {}, {}, {} diff --git a/actionview/CHANGELOG.md b/actionview/CHANGELOG.md index 82a636315a..78ce230b3a 100644 --- a/actionview/CHANGELOG.md +++ b/actionview/CHANGELOG.md @@ -1,3 +1,25 @@ +* Load the `default_form_builder` from the controller on initialization, which overrides + the global config if it is present. + + *Kevin McPhillips* + +* Accept lambda as `child_index` option in `fields_for` method. + + *Karol Galanciak* + +* `translate` allows `default: [[]]` again for a default value of `[]`. + + Fixes #19640. + + *Adam Prescott* + +* `translate` should accept nils as members of the `:default` + parameter without raising a translation missing error. + + Fixes #19419 + + *Justin Coyne* + * `number_to_percentage` does not crash with `Float::NAN` or `Float::INFINITY` as input when `precision: 0` is used. diff --git a/actionview/actionview.gemspec b/actionview/actionview.gemspec index 8f9194cda7..612e94021d 100644 --- a/actionview/actionview.gemspec +++ b/actionview/actionview.gemspec @@ -7,7 +7,7 @@ Gem::Specification.new do |s| s.summary = 'Rendering framework putting the V in MVC (part of Rails).' s.description = 'Simple, battle-tested conventions and helpers for building web pages.' - s.required_ruby_version = '>= 2.2.0' + s.required_ruby_version = '>= 2.2.2' s.license = 'MIT' @@ -23,7 +23,7 @@ Gem::Specification.new do |s| s.add_dependency 'builder', '~> 3.1' s.add_dependency 'erubis', '~> 2.7.0' - s.add_dependency 'rails-html-sanitizer', '~> 1.0', '>= 1.0.1' + s.add_dependency 'rails-html-sanitizer', '~> 1.0', '>= 1.0.2' s.add_dependency 'rails-dom-testing', '~> 1.0', '>= 1.0.5' s.add_development_dependency 'actionpack', version diff --git a/actionview/lib/action_view/helpers/asset_tag_helper.rb b/actionview/lib/action_view/helpers/asset_tag_helper.rb index 5c28043f8a..60fc9ee1a2 100644 --- a/actionview/lib/action_view/helpers/asset_tag_helper.rb +++ b/actionview/lib/action_view/helpers/asset_tag_helper.rb @@ -127,7 +127,7 @@ module ActionView # auto_discovery_link_tag(:rss, {controller: "news", action: "feed"}) # # => <link rel="alternate" type="application/rss+xml" title="RSS" href="http://www.currenthost.com/news/feed" /> # auto_discovery_link_tag(:rss, "http://www.example.com/feed.rss", {title: "Example RSS"}) - # # => <link rel="alternate" type="application/rss+xml" title="Example RSS" href="http://www.example.com/feed" /> + # # => <link rel="alternate" type="application/rss+xml" title="Example RSS" href="http://www.example.com/feed.rss" /> def auto_discovery_link_tag(type = :rss, url_options = {}, tag_options = {}) if !(type == :rss || type == :atom) && tag_options[:type].blank? raise ArgumentError.new("You should pass :type tag_option key explicitly, because you have passed #{type} type other than :rss or :atom.") diff --git a/actionview/lib/action_view/helpers/asset_url_helper.rb b/actionview/lib/action_view/helpers/asset_url_helper.rb index 29733442c1..ef4a6c98c0 100644 --- a/actionview/lib/action_view/helpers/asset_url_helper.rb +++ b/actionview/lib/action_view/helpers/asset_url_helper.rb @@ -248,6 +248,11 @@ module ActionView # 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. + # Since +javascript_url+ is based on +asset_url+ method you can set :host options. If :host + # options is set, it overwrites global +config.action_controller.asset_host+ setting. + # + # javascript_url "js/xmlhr.js", host: "http://stage.example.com" # => http://stage.example.com/assets/dir/xmlhr.js + # def javascript_url(source, options = {}) url_to_asset(source, {type: :javascript}.merge!(options)) end @@ -270,6 +275,11 @@ module ActionView # Computes the full URL to a stylesheet asset in the public stylesheets directory. # This will use +stylesheet_path+ internally, so most of their behaviors will be the same. + # Since +stylesheet_url+ is based on +asset_url+ method you can set :host options. If :host + # options is set, it overwrites global +config.action_controller.asset_host+ setting. + # + # stylesheet_url "css/style.css", host: "http://stage.example.com" # => http://stage.example.com/css/style.css + # def stylesheet_url(source, options = {}) url_to_asset(source, {type: :stylesheet}.merge!(options)) end @@ -295,6 +305,11 @@ module ActionView # Computes the full URL to an image asset. # This will use +image_path+ internally, so most of their behaviors will be the same. + # Since +image_url+ is based on +asset_url+ method you can set :host options. If :host + # options is set, it overwrites global +config.action_controller.asset_host+ setting. + # + # image_url "edit.png", host: "http://stage.example.com" # => http://stage.example.com/edit.png + # def image_url(source, options = {}) url_to_asset(source, {type: :image}.merge!(options)) end @@ -316,6 +331,11 @@ module ActionView # Computes the full URL to a video asset in the public videos directory. # This will use +video_path+ internally, so most of their behaviors will be the same. + # Since +video_url+ is based on +asset_url+ method you can set :host options. If :host + # options is set, it overwrites global +config.action_controller.asset_host+ setting. + # + # video_url "hd.avi", host: "http://stage.example.com" # => http://stage.example.com/hd.avi + # def video_url(source, options = {}) url_to_asset(source, {type: :video}.merge!(options)) end @@ -337,6 +357,11 @@ module ActionView # Computes the full URL to an audio asset in the public audios directory. # This will use +audio_path+ internally, so most of their behaviors will be the same. + # Since +audio_url+ is based on +asset_url+ method you can set :host options. If :host + # options is set, it overwrites global +config.action_controller.asset_host+ setting. + # + # audio_url "horse.wav", host: "http://stage.example.com" # => http://stage.example.com/horse.wav + # def audio_url(source, options = {}) url_to_asset(source, {type: :audio}.merge!(options)) end @@ -357,6 +382,11 @@ module ActionView # Computes the full URL to a font asset. # This will use +font_path+ internally, so most of their behaviors will be the same. + # Since +font_url+ is based on +asset_url+ method you can set :host options. If :host + # options is set, it overwrites global +config.action_controller.asset_host+ setting. + # + # font_url "font.ttf", host: "http://stage.example.com" # => http://stage.example.com/font.ttf + # def font_url(source, options = {}) url_to_asset(source, {type: :font}.merge!(options)) end diff --git a/actionview/lib/action_view/helpers/controller_helper.rb b/actionview/lib/action_view/helpers/controller_helper.rb index 74ef25f7c1..3569fba8c6 100644 --- a/actionview/lib/action_view/helpers/controller_helper.rb +++ b/actionview/lib/action_view/helpers/controller_helper.rb @@ -14,6 +14,7 @@ module ActionView if @_controller = controller @_request = controller.request if controller.respond_to?(:request) @_config = controller.config.inheritable_copy if controller.respond_to?(:config) + @_default_form_builder = controller.default_form_builder if controller.respond_to?(:default_form_builder) end end diff --git a/actionview/lib/action_view/helpers/form_helper.rb b/actionview/lib/action_view/helpers/form_helper.rb index b0793d0b91..7fdeca5ea8 100644 --- a/actionview/lib/action_view/helpers/form_helper.rb +++ b/actionview/lib/action_view/helpers/form_helper.rb @@ -67,9 +67,10 @@ module ActionView # # In particular, thanks to the conventions followed in the generated field names, the # controller gets a nested hash <tt>params[:person]</tt> with the person attributes - # set in the form. That hash is ready to be passed to <tt>Person.create</tt>: + # set in the form. That hash is ready to be passed to <tt>Person.new</tt>: # - # if @person = Person.create(params[:person]) + # @person = Person.new(params[:person]) + # if @person.save # # success # else # # error handling @@ -113,6 +114,8 @@ module ActionView include ModelNaming include RecordIdentifier + attr_internal :default_form_builder + # Creates a form that allows the user to create or update the attributes # of a specific model object. # @@ -140,6 +143,7 @@ module ActionView # will get expanded to # # <%= text_field :person, :first_name %> + # # which results in an HTML <tt><input></tt> tag whose +name+ attribute is # <tt>person[first_name]</tt>. This means that when the form is submitted, # the value entered by the user will be available in the controller as @@ -1231,7 +1235,7 @@ module ActionView end def default_form_builder_class - builder = ActionView::Base.default_form_builder + builder = default_form_builder || ActionView::Base.default_form_builder builder.respond_to?(:constantize) ? builder.constantize : builder end end @@ -1926,7 +1930,11 @@ module ActionView explicit_child_index = options[:child_index] output = ActiveSupport::SafeBuffer.new association.each do |child| - options[:child_index] = nested_child_index(name) unless explicit_child_index + if explicit_child_index + options[:child_index] = explicit_child_index.call if explicit_child_index.respond_to?(:call) + else + options[:child_index] = nested_child_index(name) + end output << fields_for_nested_model("#{name}[#{options[:child_index]}]", child, options, block) end output diff --git a/actionview/lib/action_view/helpers/form_options_helper.rb b/actionview/lib/action_view/helpers/form_options_helper.rb index bbfbf482a4..8a5928477f 100644 --- a/actionview/lib/action_view/helpers/form_options_helper.rb +++ b/actionview/lib/action_view/helpers/form_options_helper.rb @@ -18,10 +18,10 @@ module ActionView # # could become: # - # <select name="post[category]"> - # <option></option> - # <option>joke</option> - # <option>poem</option> + # <select name="post[category]" id="post_category"> + # <option value=""></option> + # <option value="joke">joke</option> + # <option value="poem">poem</option> # </select> # # Another common case is a select tag for a <tt>belongs_to</tt>-associated object. @@ -32,7 +32,7 @@ module ActionView # # could become: # - # <select name="post[person_id]"> + # <select name="post[person_id]" id="post_person_id"> # <option value="">None</option> # <option value="1">David</option> # <option value="2" selected="selected">Sam</option> @@ -45,7 +45,7 @@ module ActionView # # could become: # - # <select name="post[person_id]"> + # <select name="post[person_id]" id="post_person_id"> # <option value="">Select Person</option> # <option value="1">David</option> # <option value="2">Sam</option> @@ -71,11 +71,11 @@ module ActionView # # could become: # - # <select name="post[category]"> - # <option></option> - # <option>joke</option> - # <option>poem</option> - # <option disabled="disabled">restricted</option> + # <select name="post[category]" id="post_category"> + # <option value=""></option> + # <option value="joke">joke</option> + # <option value="poem">poem</option> + # <option disabled="disabled" value="restricted">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. @@ -83,7 +83,7 @@ module ActionView # 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]"> + # <select name="post[category_id]" id="post_category_id"> # <option value="1" disabled="disabled">2008 stuff</option> # <option value="2" disabled="disabled">Christmas</option> # <option value="3">Jokes</option> @@ -109,7 +109,7 @@ module ActionView # # would become: # - # <select name="post[person_id]"> + # <select name="post[person_id]" id="post_person_id"> # <option value=""></option> # <option value="1" selected="selected">David</option> # <option value="2">Sam</option> @@ -192,7 +192,7 @@ module ActionView # collection_select(:post, :author_id, Author.all, :id, :name_with_initial, prompt: true) # # If <tt>@post.author_id</tt> is already <tt>1</tt>, this would return: - # <select name="post[author_id]"> + # <select name="post[author_id]" id="post_author_id"> # <option value="">Please select</option> # <option value="1" selected="selected">D. Heinemeier Hansson</option> # <option value="2">D. Thomas</option> @@ -243,7 +243,7 @@ module ActionView # # Possible output: # - # <select name="city[country_id]"> + # <select name="city[country_id]" id="city_country_id"> # <optgroup label="Africa"> # <option value="1">South Africa</option> # <option value="3">Somalia</option> @@ -302,17 +302,17 @@ module ActionView # # => <option value="DKK">Kroner</option> # # options_for_select([ "VISA", "MasterCard" ], "MasterCard") - # # => <option>VISA</option> - # # => <option selected="selected">MasterCard</option> + # # => <option value="VISA">VISA</option> + # # => <option selected="selected" value="MasterCard">MasterCard</option> # # options_for_select({ "Basic" => "$20", "Plus" => "$40" }, "$40") # # => <option value="$20">Basic</option> # # => <option value="$40" selected="selected">Plus</option> # # options_for_select([ "VISA", "MasterCard", "Discover" ], ["VISA", "Discover"]) - # # => <option selected="selected">VISA</option> - # # => <option>MasterCard</option> - # # => <option selected="selected">Discover</option> + # # => <option selected="selected" value="VISA">VISA</option> + # # => <option value="MasterCard">MasterCard</option> + # # => <option selected="selected" value="Discover">Discover</option> # # You can optionally provide HTML attributes as the last element of the array. # diff --git a/actionview/lib/action_view/helpers/form_tag_helper.rb b/actionview/lib/action_view/helpers/form_tag_helper.rb index 65a0548ffb..1f76f40138 100644 --- a/actionview/lib/action_view/helpers/form_tag_helper.rb +++ b/actionview/lib/action_view/helpers/form_tag_helper.rb @@ -80,7 +80,7 @@ module ActionView # associated records. <tt>option_tags</tt> is a string containing the option tags for the select box. # # ==== Options - # * <tt>:multiple</tt> - If set to true the selection will allow multiple choices. + # * <tt>:multiple</tt> - If set to true, the selection will allow multiple choices. # * <tt>:disabled</tt> - If set to true, the user will not be able to use this input. # * <tt>:include_blank</tt> - If set to true, an empty option will be created. If set to a string, the string will be used as the option's content and the value will be empty. # * <tt>:prompt</tt> - Create a prompt option with blank value and the text asking user to select something. diff --git a/actionview/lib/action_view/helpers/javascript_helper.rb b/actionview/lib/action_view/helpers/javascript_helper.rb index 629c447f3f..e237a32cb7 100644 --- a/actionview/lib/action_view/helpers/javascript_helper.rb +++ b/actionview/lib/action_view/helpers/javascript_helper.rb @@ -21,7 +21,7 @@ module ActionView # Also available through the alias j(). This is particularly helpful in JavaScript # responses, like: # - # $('some_element').replaceWith('<%=j render 'some/element_template' %>'); + # $('some_element').replaceWith('<%= j render 'some/element_template' %>'); def escape_javascript(javascript) if javascript result = javascript.gsub(/(\\|<\/|\r\n|\342\200\250|\342\200\251|[\n\r"'])/u) {|match| JS_ESCAPE_MAP[match] } diff --git a/actionview/lib/action_view/helpers/sanitize_helper.rb b/actionview/lib/action_view/helpers/sanitize_helper.rb index 463a4e9f60..a2e9f37453 100644 --- a/actionview/lib/action_view/helpers/sanitize_helper.rb +++ b/actionview/lib/action_view/helpers/sanitize_helper.rb @@ -99,7 +99,7 @@ module ActionView # strip_tags("<div id='top-bar'>Welcome to my website!</div>") # # => Welcome to my website! def strip_tags(html) - self.class.full_sanitizer.sanitize(html) + self.class.full_sanitizer.sanitize(html, encode_special_chars: false) end # Strips all link tags from +html+ leaving just the link text. diff --git a/actionview/lib/action_view/helpers/translation_helper.rb b/actionview/lib/action_view/helpers/translation_helper.rb index 24b633c5bb..9d7390f1fd 100644 --- a/actionview/lib/action_view/helpers/translation_helper.rb +++ b/actionview/lib/action_view/helpers/translation_helper.rb @@ -38,10 +38,10 @@ module ActionView def translate(key, options = {}) options = options.dup has_default = options.has_key?(:default) - remaining_defaults = Array(options.delete(:default)) + remaining_defaults = Array(options.delete(:default)).compact if has_default && !remaining_defaults.first.kind_of?(Symbol) - options[:default] = remaining_defaults.shift + options[:default] = remaining_defaults end # If the user has explicitly decided to NOT raise errors, pass that option to I18n. diff --git a/actionview/lib/action_view/helpers/url_helper.rb b/actionview/lib/action_view/helpers/url_helper.rb index 3dbce0738e..afb1265ad9 100644 --- a/actionview/lib/action_view/helpers/url_helper.rb +++ b/actionview/lib/action_view/helpers/url_helper.rb @@ -172,6 +172,11 @@ module ActionView # # link_to "Visit Other Site", "http://www.rubyonrails.org/", data: { confirm: "Are you sure?" } # # => <a href="http://www.rubyonrails.org/" data-confirm="Are you sure?">Visit Other Site</a> + # + # Also you can set any link attributes such as <tt>target</tt>, <tt>rel</tt>, <tt>type</tt>: + # + # link_to "External link", "http://www.rubyonrails.org/", target: "_blank", rel: "nofollow" + # # => <a href="http://www.rubyonrails.org/" target="_blank" rel="nofollow">External link</a> def link_to(name = nil, options = nil, html_options = nil, &block) html_options, options, name = options, name, block if block_given? options ||= {} @@ -280,9 +285,7 @@ module ActionView html_options, options = options, name if block_given? options ||= {} html_options ||= {} - html_options = html_options.stringify_keys - convert_boolean_attributes!(html_options, %w(disabled)) url = options.is_a?(String) ? options : url_for(options) remote = html_options.delete('remote') @@ -294,8 +297,9 @@ module ActionView form_method = method == 'get' ? 'get' : 'post' form_options = html_options.delete('form') || {} form_options[:class] ||= html_options.delete('form_class') || 'button_to' - form_options.merge!(method: form_method, action: url) - form_options.merge!("data-remote" => "true") if remote + form_options[:method] = form_method + form_options[:action] = url + form_options[:'data-remote'] = true if remote request_token_tag = form_method == 'post' ? token_tag : '' @@ -472,57 +476,45 @@ module ActionView # True if the current request URI was generated by the given +options+. # # ==== Examples - # Let's say we're in the <tt>http://www.example.com/shop/checkout?order=desc</tt> action. + # Let's say we're in the <tt>http://www.example.com/shop/checkout?order=desc&page=1</tt> action. # # current_page?(action: 'process') # # => false # - # current_page?(controller: 'shop', action: 'checkout') - # # => true - # - # current_page?(controller: 'shop', action: 'checkout', order: 'asc') - # # => false - # # current_page?(action: 'checkout') # # => true # # current_page?(controller: 'library', action: 'checkout') # # => false # - # current_page?('http://www.example.com/shop/checkout') - # # => true - # - # current_page?('/shop/checkout') + # current_page?(controller: 'shop', action: 'checkout') # # => true # - # Let's say we're in the <tt>http://www.example.com/shop/checkout?order=desc&page=1</tt> action. - # - # current_page?(action: 'process') + # current_page?(controller: 'shop', action: 'checkout', order: 'asc') # # => false # - # current_page?(controller: 'shop', action: 'checkout') - # # => true - # # current_page?(controller: 'shop', action: 'checkout', order: 'desc', page: '1') # # => true # # current_page?(controller: 'shop', action: 'checkout', order: 'desc', page: '2') # # => false # - # current_page?(controller: 'shop', action: 'checkout', order: 'desc') - # # => false + # current_page?('http://www.example.com/shop/checkout') + # # => true # - # current_page?(action: 'checkout') + # current_page?('/shop/checkout') # # => true # - # current_page?(controller: 'library', action: 'checkout') - # # => false + # current_page?('http://www.example.com/shop/checkout?order=desc&page=1') + # # => true # # Let's say we're in the <tt>http://www.example.com/products</tt> action with method POST in case of invalid product. # # current_page?(controller: 'product', action: 'index') # # => false # + # We can also pass in the symbol arguments instead of strings. + # def current_page?(options) unless request raise "You cannot use helpers that need to determine the current " \ @@ -576,34 +568,6 @@ module ActionView html_options["data-method"] = method end - # Processes the +html_options+ hash, converting the boolean - # attributes from true/false form into the form required by - # HTML/XHTML. (An attribute is considered to be boolean if - # its name is listed in the given +bool_attrs+ array.) - # - # More specifically, for each boolean attribute in +html_options+ - # given as: - # - # "attr" => bool_value - # - # if the associated +bool_value+ evaluates to true, it is - # replaced with the attribute's name; otherwise the attribute is - # removed from the +html_options+ hash. (See the XHTML 1.0 spec, - # section 4.5 "Attribute Minimization" for more: - # http://www.w3.org/TR/xhtml1/#h-4.5) - # - # Returns the updated +html_options+ hash, which is also modified - # in place. - # - # Example: - # - # convert_boolean_attributes!( html_options, - # %w( checked disabled readonly ) ) - def convert_boolean_attributes!(html_options, bool_attrs) - bool_attrs.each { |x| html_options[x] = x if html_options.delete(x) } - html_options - end - def token_tag(token=nil) if token != false && protect_against_forgery? token ||= form_authenticity_token diff --git a/actionview/lib/action_view/layouts.rb b/actionview/lib/action_view/layouts.rb index 9d636c8c9e..1fc609f2cd 100644 --- a/actionview/lib/action_view/layouts.rb +++ b/actionview/lib/action_view/layouts.rb @@ -315,16 +315,25 @@ module ActionView name_clause end - self.class_eval <<-RUBY, __FILE__, __LINE__ + 1 - def _layout - if _conditional_layout? + if self._layout_conditions.empty? + self.class_eval <<-RUBY, __FILE__, __LINE__ + 1 + def _layout #{layout_definition} - else - #{name_clause} end - end - private :_layout - RUBY + private :_layout + RUBY + else + self.class_eval <<-RUBY, __FILE__, __LINE__ + 1 + def _layout + if _conditional_layout? + #{layout_definition} + else + #{name_clause} + end + end + private :_layout + RUBY + end end private diff --git a/actionview/lib/action_view/lookup_context.rb b/actionview/lib/action_view/lookup_context.rb index 36855ec3d0..4452dcfed5 100644 --- a/actionview/lib/action_view/lookup_context.rb +++ b/actionview/lib/action_view/lookup_context.rb @@ -126,7 +126,7 @@ module ActionView @view_paths.find_all(*args_for_lookup(name, prefixes, partial, keys, options)) end - def exists?(name, prefixes = [], partial = false, keys = [], options = {}) + def exists?(name, prefixes = [], partial = false, keys = [], **options) @view_paths.exists?(*args_for_lookup(name, prefixes, partial, keys, options)) end alias :template_exists? :exists? diff --git a/actionview/lib/action_view/renderer/partial_renderer.rb b/actionview/lib/action_view/renderer/partial_renderer.rb index 56b8ab1e2d..cd151c0189 100644 --- a/actionview/lib/action_view/renderer/partial_renderer.rb +++ b/actionview/lib/action_view/renderer/partial_renderer.rb @@ -154,23 +154,23 @@ module ActionView # specified globally for the entire action, but they work in a similar fashion. Imagine a list with two types # of users: # - # <%# app/views/users/index.html.erb &> + # <%# app/views/users/index.html.erb %> # Here's the administrator: # <%= render partial: "user", layout: "administrator", locals: { user: administrator } %> # # Here's the editor: # <%= render partial: "user", layout: "editor", locals: { user: editor } %> # - # <%# app/views/users/_user.html.erb &> + # <%# app/views/users/_user.html.erb %> # Name: <%= user.name %> # - # <%# app/views/users/_administrator.html.erb &> + # <%# app/views/users/_administrator.html.erb %> # <div id="administrator"> # Budget: $<%= user.budget %> # <%= yield %> # </div> # - # <%# app/views/users/_editor.html.erb &> + # <%# app/views/users/_editor.html.erb %> # <div id="editor"> # Deadline: <%= user.deadline %> # <%= yield %> @@ -233,7 +233,7 @@ module ActionView # # You can also apply a layout to a block within any template: # - # <%# app/views/users/_chief.html.erb &> + # <%# app/views/users/_chief.html.erb %> # <%= render(layout: "administrator", locals: { user: chief }) do %> # Title: <%= chief.title %> # <% end %> @@ -250,13 +250,13 @@ module ActionView # If you pass arguments to "yield" then this will be passed to the block. One way to use this is to pass # an array to layout and treat it as an enumerable. # - # <%# app/views/users/_user.html.erb &> + # <%# app/views/users/_user.html.erb %> # <div class="user"> # Budget: $<%= user.budget %> # <%= yield user %> # </div> # - # <%# app/views/users/index.html.erb &> + # <%# app/views/users/index.html.erb %> # <%= render layout: @users do |user| %> # Title: <%= user.title %> # <% end %> @@ -265,14 +265,14 @@ module ActionView # # You can also yield multiple times in one layout and use block arguments to differentiate the sections. # - # <%# app/views/users/_user.html.erb &> + # <%# app/views/users/_user.html.erb %> # <div class="user"> # <%= yield user, :header %> # Budget: $<%= user.budget %> # <%= yield user, :footer %> # </div> # - # <%# app/views/users/index.html.erb &> + # <%# app/views/users/index.html.erb %> # <%= render layout: @users do |user, section| %> # <%- case section when :header -%> # Title: <%= user.title %> diff --git a/actionview/lib/action_view/renderer/renderer.rb b/actionview/lib/action_view/renderer/renderer.rb index 964b18337e..1bee35d80d 100644 --- a/actionview/lib/action_view/renderer/renderer.rb +++ b/actionview/lib/action_view/renderer/renderer.rb @@ -37,7 +37,7 @@ module ActionView end end - # Direct accessor to template rendering. + # Direct access to template rendering. def render_template(context, options) #:nodoc: TemplateRenderer.new(@lookup_context).render(context, options) end diff --git a/actionview/lib/action_view/renderer/template_renderer.rb b/actionview/lib/action_view/renderer/template_renderer.rb index cd21d7ab47..dbb4855e39 100644 --- a/actionview/lib/action_view/renderer/template_renderer.rb +++ b/actionview/lib/action_view/renderer/template_renderer.rb @@ -40,7 +40,7 @@ module ActionView find_template(options[:template], options[:prefixes], false, keys, @details) end else - raise ArgumentError, "You invoked render but did not give any of :partial, :template, :inline, :file, :plain, :text or :body option." + raise ArgumentError, "You invoked render but did not give any of :partial, :template, :inline, :file, :plain, :html, :text or :body option." end end diff --git a/actionview/lib/action_view/template/handlers/raw.rb b/actionview/lib/action_view/template/handlers/raw.rb index 397c86014a..b08fb0870f 100644 --- a/actionview/lib/action_view/template/handlers/raw.rb +++ b/actionview/lib/action_view/template/handlers/raw.rb @@ -2,7 +2,7 @@ module ActionView module Template::Handlers class Raw def call(template) - escaped = template.source.gsub(/:/, '\:') + escaped = template.source.gsub(':'.freeze, '\:'.freeze) '%q:' + escaped + ':;' end diff --git a/actionview/lib/action_view/template/resolver.rb b/actionview/lib/action_view/template/resolver.rb index bc0db330ea..955118a554 100644 --- a/actionview/lib/action_view/template/resolver.rb +++ b/actionview/lib/action_view/template/resolver.rb @@ -270,7 +270,7 @@ module ActionView # # ActionController::Base.view_paths = FileSystemResolver.new( # Rails.root.join("app/views"), - # ":prefix{/:locale}/:action{.:formats,}{+:variants,}{.:handlers,}" + # ":prefix/:action{.:locale,}{.:formats,}{+:variants,}{.:handlers,}", # ) # # ==== Pattern format and variables diff --git a/actionview/test/abstract_unit.rb b/actionview/test/abstract_unit.rb index 3eded74f1b..4635c645d0 100644 --- a/actionview/test/abstract_unit.rb +++ b/actionview/test/abstract_unit.rb @@ -49,20 +49,6 @@ I18n.backend.store_translations 'pt-BR', {} ORIGINAL_LOCALES = I18n.available_locales.map(&:to_s).sort FIXTURE_LOAD_PATH = File.join(File.dirname(__FILE__), 'fixtures') -FIXTURES = Pathname.new(FIXTURE_LOAD_PATH) - -module RackTestUtils - def body_to_string(body) - if body.respond_to?(:each) - str = "" - body.each {|s| str << s } - str - else - body - end - end - extend self -end module RenderERBUtils def view @@ -225,49 +211,6 @@ class ActionDispatch::IntegrationTest < ActiveSupport::TestCase end end -# Temporary base class -class Rack::TestCase < ActionDispatch::IntegrationTest - def self.testing(klass = nil) - if klass - @testing = "/#{klass.name.underscore}".sub!(/_controller$/, '') - else - @testing - end - end - - def get(thing, *args) - if thing.is_a?(Symbol) - super("#{self.class.testing}/#{thing}", *args) - else - super - end - end - - def assert_body(body) - assert_equal body, Array(response.body).join - end - - def assert_status(code) - assert_equal code, response.status - end - - def assert_response(body, status = 200, headers = {}) - assert_body body - assert_status status - headers.each do |header, value| - assert_header header, value - end - end - - def assert_content_type(type) - assert_equal type, response.headers["Content-Type"] - end - - def assert_header(name, value) - assert_equal value, response.headers[name] - end -end - ActionView::RoutingUrlFor.include(ActionDispatch::Routing::UrlFor) module ActionController diff --git a/actionview/test/actionpack/abstract/views/abstract_controller/testing/me5/index.erb b/actionview/test/actionpack/abstract/views/abstract_controller/testing/me5/index.erb deleted file mode 100644 index 84d0b7417e..0000000000 --- a/actionview/test/actionpack/abstract/views/abstract_controller/testing/me5/index.erb +++ /dev/null @@ -1 +0,0 @@ -Hello from me5/index.erb
\ No newline at end of file diff --git a/actionview/test/actionpack/controller/layout_test.rb b/actionview/test/actionpack/controller/layout_test.rb index 7b8a83e2fe..64ab125637 100644 --- a/actionview/test/actionpack/controller/layout_test.rb +++ b/actionview/test/actionpack/controller/layout_test.rb @@ -122,6 +122,14 @@ class PrependsViewPathController < LayoutTest end end +class ParentController < LayoutTest + layout 'item' +end + +class ChildController < ParentController + layout 'layout_test', only: :hello +end + class OnlyLayoutController < LayoutTest layout 'item', :only => "hello" end @@ -225,6 +233,12 @@ class LayoutSetInResponseTest < ActionController::TestCase get :hello assert_equal "layout_test.erb hello.erb", @response.body.strip end + + def test_respect_to_parent_layout + @controller = ChildController.new + get :goodbye + assert_template :layout => "layouts/item" + end end class SetsNonExistentLayoutFile < LayoutTest diff --git a/actionview/test/active_record_unit.rb b/actionview/test/active_record_unit.rb index cca55c9af4..f9e94413b5 100644 --- a/actionview/test/active_record_unit.rb +++ b/actionview/test/active_record_unit.rb @@ -76,7 +76,7 @@ class ActiveRecordTestCase < ActionController::TestCase # Set our fixture path if ActiveRecordTestConnector.able_to_connect self.fixture_path = [FIXTURE_LOAD_PATH] - self.use_transactional_fixtures = false + self.use_transactional_tests = false end def self.fixtures(*args) diff --git a/actionview/test/fixtures/multipart/bracketed_utf8_param b/actionview/test/fixtures/multipart/bracketed_utf8_param deleted file mode 100644 index df9cecea08..0000000000 --- a/actionview/test/fixtures/multipart/bracketed_utf8_param +++ /dev/null @@ -1,5 +0,0 @@ ---AaB03x -Content-Disposition: form-data; name="Iñtërnâtiônà lizætiøn_name[Iñtërnâtiônà lizætiøn_nested_name]" - -Iñtërnâtiônà lizætiøn_value ---AaB03x-- diff --git a/actionview/test/fixtures/multipart/single_utf8_param b/actionview/test/fixtures/multipart/single_utf8_param deleted file mode 100644 index 1d9fae7b17..0000000000 --- a/actionview/test/fixtures/multipart/single_utf8_param +++ /dev/null @@ -1,5 +0,0 @@ ---AaB03x -Content-Disposition: form-data; name="Iñtërnâtiônà lizætiøn_name" - -Iñtërnâtiônà lizætiøn_value ---AaB03x-- diff --git a/actionview/test/template/asset_tag_helper_test.rb b/actionview/test/template/asset_tag_helper_test.rb index a15c6fac90..02dce71496 100644 --- a/actionview/test/template/asset_tag_helper_test.rb +++ b/actionview/test/template/asset_tag_helper_test.rb @@ -1,4 +1,3 @@ -require 'zlib' require 'abstract_unit' require 'active_support/ordered_options' diff --git a/actionview/test/template/controller_helper_test.rb b/actionview/test/template/controller_helper_test.rb new file mode 100644 index 0000000000..b5e94ea4f1 --- /dev/null +++ b/actionview/test/template/controller_helper_test.rb @@ -0,0 +1,21 @@ +require 'abstract_unit' + +class ControllerHelperTest < ActionView::TestCase + tests ActionView::Helpers::ControllerHelper + + class SpecializedFormBuilder < ActionView::Helpers::FormBuilder ; end + + def test_assign_controller_sets_default_form_builder + @controller = OpenStruct.new(default_form_builder: SpecializedFormBuilder) + assign_controller(@controller) + + assert_equal SpecializedFormBuilder, self.default_form_builder + end + + def test_assign_controller_skips_default_form_builder + @controller = OpenStruct.new + assign_controller(@controller) + + assert_nil self.default_form_builder + end +end diff --git a/actionview/test/template/date_helper_test.rb b/actionview/test/template/date_helper_test.rb index bfb073680e..a0e1f5f3bb 100644 --- a/actionview/test/template/date_helper_test.rb +++ b/actionview/test/template/date_helper_test.rb @@ -130,7 +130,7 @@ class DateHelperTest < ActionView::TestCase def test_distance_in_words_with_mathn_required # test we avoid Integer#/ (redefined by mathn) - require 'mathn' + silence_warnings { require "mathn" } from = Time.utc(2004, 6, 6, 21, 45, 0) assert_distance_of_time_in_words(from) end diff --git a/actionview/test/template/form_helper_test.rb b/actionview/test/template/form_helper_test.rb index 4e336bea63..b8cb5bd746 100644 --- a/actionview/test/template/form_helper_test.rb +++ b/actionview/test/template/form_helper_test.rb @@ -2878,6 +2878,23 @@ class FormHelperTest < ActionView::TestCase assert_dom_equal expected, output_buffer end + def test_nested_fields_for_with_child_index_as_lambda_option_override_on_a_nested_attributes_collection_association + @post.comments = [] + + form_for(@post) do |f| + concat f.fields_for(:comments, Comment.new(321), child_index: -> { 'abc' } ) { |cf| + concat cf.text_field(:name) + } + end + + expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', method: 'patch') do + '<input id="post_comments_attributes_abc_name" name="post[comments_attributes][abc][name]" type="text" value="comment #321" />' + + '<input id="post_comments_attributes_abc_id" name="post[comments_attributes][abc][id]" type="hidden" value="321" />' + end + + assert_dom_equal expected, output_buffer + end + class FakeAssociationProxy def to_ary [1, 2, 3] @@ -3252,6 +3269,30 @@ class FormHelperTest < ActionView::TestCase ActionView::Base.default_form_builder = old_default_form_builder end + def test_form_builder_override + self.default_form_builder = LabelledFormBuilder + + output_buffer = fields_for(:post, @post) do |f| + concat f.text_field(:title) + end + + expected = "<label for='title'>Title:</label> <input name='post[title]' type='text' id='post_title' value='Hello World' /><br/>" + + assert_dom_equal expected, output_buffer + end + + def test_lazy_loading_form_builder_override + self.default_form_builder = "FormHelperTest::LabelledFormBuilder" + + output_buffer = fields_for(:post, @post) do |f| + concat f.text_field(:title) + end + + expected = "<label for='title'>Title:</label> <input name='post[title]' type='text' id='post_title' value='Hello World' /><br/>" + + assert_dom_equal expected, output_buffer + end + def test_fields_for_with_labelled_builder output_buffer = fields_for(:post, @post, builder: LabelledFormBuilder) do |f| concat f.text_field(:title) diff --git a/actionview/test/template/form_tag_helper_test.rb b/actionview/test/template/form_tag_helper_test.rb index 84a581b107..cad1c82309 100644 --- a/actionview/test/template/form_tag_helper_test.rb +++ b/actionview/test/template/form_tag_helper_test.rb @@ -210,13 +210,13 @@ class FormTagHelperTest < ActionView::TestCase end def test_select_tag_with_multiple - actual = select_tag "colors", "<option>Red</option><option>Blue</option><option>Green</option>".html_safe, :multiple => :true - expected = %(<select id="colors" multiple="multiple" name="colors"><option>Red</option><option>Blue</option><option>Green</option></select>) + actual = select_tag "colors", "<option>Red</option><option>Blue</option><option>Green</option>".html_safe, multiple: true + expected = %(<select id="colors" multiple="multiple" name="colors[]"><option>Red</option><option>Blue</option><option>Green</option></select>) assert_dom_equal expected, actual end def test_select_tag_disabled - actual = select_tag "places", "<option>Home</option><option>Work</option><option>Pub</option>".html_safe, :disabled => :true + actual = select_tag "places", "<option>Home</option><option>Work</option><option>Pub</option>".html_safe, disabled: true expected = %(<select id="places" disabled="disabled" name="places"><option>Home</option><option>Work</option><option>Pub</option></select>) assert_dom_equal expected, actual end @@ -352,7 +352,7 @@ class FormTagHelperTest < ActionView::TestCase end def test_text_field_disabled - actual = text_field_tag "title", "Hello!", :disabled => :true + actual = text_field_tag "title", "Hello!", disabled: true expected = %(<input id="title" name="title" disabled="disabled" type="text" value="Hello!" />) assert_dom_equal expected, actual end diff --git a/actionview/test/template/javascript_helper_test.rb b/actionview/test/template/javascript_helper_test.rb index 9ba7f64ad1..9f1535ef53 100644 --- a/actionview/test/template/javascript_helper_test.rb +++ b/actionview/test/template/javascript_helper_test.rb @@ -3,14 +3,7 @@ require 'abstract_unit' class JavaScriptHelperTest < ActionView::TestCase tests ActionView::Helpers::JavaScriptHelper - def _evaluate_assigns_and_ivars() end - - attr_accessor :formats, :output_buffer - - def update_details(details) - @details = details - yield if block_given? - end + attr_accessor :output_buffer setup do @old_escape_html_entities_in_json = ActiveSupport.escape_html_entities_in_json diff --git a/actionview/test/template/sanitize_helper_test.rb b/actionview/test/template/sanitize_helper_test.rb index e4be21be2c..efe846a7eb 100644 --- a/actionview/test/template/sanitize_helper_test.rb +++ b/actionview/test/template/sanitize_helper_test.rb @@ -29,6 +29,10 @@ class SanitizeHelperTest < ActionView::TestCase assert_equal "", strip_tags("<script>") end + def test_strip_tags_will_not_encode_special_characters + assert_equal "test\r\n\r\ntest", strip_tags("test\r\n\r\ntest") + end + def test_sanitize_is_marked_safe assert sanitize("<html><script></script></html>").html_safe? end diff --git a/actionview/test/template/translation_helper_test.rb b/actionview/test/template/translation_helper_test.rb index ef4d13efa7..df096b3c3a 100644 --- a/actionview/test/template/translation_helper_test.rb +++ b/actionview/test/template/translation_helper_test.rb @@ -190,6 +190,16 @@ class TranslationHelperTest < ActiveSupport::TestCase assert_equal 'A Generic String', translation end + def test_translate_with_array_of_defaults_with_nil + translation = translate(:'translations.missing', default: [:'also_missing', nil, 'A Generic String']) + assert_equal 'A Generic String', translation + end + + def test_translate_with_array_of_array_default + translation = translate(:'translations.missing', default: [[]]) + assert_equal [], translation + end + def test_translate_does_not_change_options options = {} translate(:'translations.missing', options) diff --git a/activejob/CHANGELOG.md b/activejob/CHANGELOG.md index d4e19274fa..85a437a1dd 100644 --- a/activejob/CHANGELOG.md +++ b/activejob/CHANGELOG.md @@ -1,3 +1,7 @@ +* A generated job now inherents from `app/jobs/application_job.rb` by default. + + *Jeroen van Baarsen* + * Add an `:only` option to `perform_enqueued_jobs` to filter jobs based on type. diff --git a/activejob/activejob.gemspec b/activejob/activejob.gemspec index 5404ece804..24e38e495f 100644 --- a/activejob/activejob.gemspec +++ b/activejob/activejob.gemspec @@ -7,7 +7,7 @@ Gem::Specification.new do |s| 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 = '>= 2.2.0' + s.required_ruby_version = '>= 2.2.2' s.license = 'MIT' diff --git a/activejob/lib/active_job/logging.rb b/activejob/lib/active_job/logging.rb index cd29e6908e..54774db601 100644 --- a/activejob/lib/active_job/logging.rb +++ b/activejob/lib/active_job/logging.rb @@ -81,7 +81,7 @@ module ActiveJob private def queue_name(event) - event.payload[:adapter].name.demodulize.remove('Adapter') + "(#{event.payload[:job].queue_name})" + event.payload[:adapter].class.name.demodulize.remove('Adapter') + "(#{event.payload[:job].queue_name})" end def args_info(job) diff --git a/activejob/lib/active_job/queue_adapter.rb b/activejob/lib/active_job/queue_adapter.rb index aa3ebdbc7b..9c4519432d 100644 --- a/activejob/lib/active_job/queue_adapter.rb +++ b/activejob/lib/active_job/queue_adapter.rb @@ -1,4 +1,5 @@ require 'active_job/queue_adapters/inline_adapter' +require 'active_support/core_ext/class/attribute' require 'active_support/core_ext/string/inflections' module ActiveJob @@ -7,27 +8,54 @@ module ActiveJob module QueueAdapter #:nodoc: extend ActiveSupport::Concern + included do + class_attribute :_queue_adapter, instance_accessor: false, instance_predicate: false + self.queue_adapter = :inline + end + # Includes the setter method for changing the active queue adapter. module ClassMethods - mattr_reader(:queue_adapter) { ActiveJob::QueueAdapters::InlineAdapter } + def queue_adapter + _queue_adapter + end # Specify the backend queue provider. The default queue adapter # is the :inline queue. See QueueAdapters for more # information. - def queue_adapter=(name_or_adapter) - @@queue_adapter = \ - case name_or_adapter - when Symbol, String - load_adapter(name_or_adapter) - else - name_or_adapter if name_or_adapter.respond_to?(:enqueue) - end + def queue_adapter=(name_or_adapter_or_class) + self._queue_adapter = interpret_adapter(name_or_adapter_or_class) end private - def load_adapter(name) - "ActiveJob::QueueAdapters::#{name.to_s.camelize}Adapter".constantize + + def interpret_adapter(name_or_adapter_or_class) + case name_or_adapter_or_class + when Symbol, String + ActiveJob::QueueAdapters.lookup(name_or_adapter_or_class).new + else + if queue_adapter?(name_or_adapter_or_class) + name_or_adapter_or_class + elsif queue_adapter_class?(name_or_adapter_or_class) + ActiveSupport::Deprecation.warn "Passing an adapter class is deprecated " \ + "and will be removed in Rails 5.1. Please pass an adapter name " \ + "(.queue_adapter = :#{name_or_adapter_or_class.name.demodulize.remove('Adapter').underscore}) " \ + "or an instance (.queue_adapter = #{name_or_adapter_or_class.name}.new) instead." + name_or_adapter_or_class.new + else + raise ArgumentError + end end + end + + QUEUE_ADAPTER_METHODS = [:enqueue, :enqueue_at].freeze + + def queue_adapter?(object) + QUEUE_ADAPTER_METHODS.all? { |meth| object.respond_to?(meth) } + end + + def queue_adapter_class?(object) + object.is_a?(Class) && QUEUE_ADAPTER_METHODS.all? { |meth| object.public_method_defined?(meth) } + end end end end diff --git a/activejob/lib/active_job/queue_adapters.rb b/activejob/lib/active_job/queue_adapters.rb index 4b91c93dbe..bd69e525bb 100644 --- a/activejob/lib/active_job/queue_adapters.rb +++ b/activejob/lib/active_job/queue_adapters.rb @@ -27,13 +27,76 @@ module ActiveJob # | Sneakers | Yes | Yes | No | Queue | Queue | No | # | Sucker Punch | Yes | Yes | No | No | No | No | # | Active Job Inline | No | Yes | N/A | N/A | N/A | N/A | - # | Active Job | Yes | Yes | Yes | No | No | No | + # + # ==== Async + # + # Yes: The Queue Adapter runs the jobs in a separate or forked process. + # + # No: The job is run in the same process. + # + # ==== Queues + # + # Yes: Jobs may set which queue they are run in with queue_as or by using the set + # method. + # + # ==== Delayed + # + # Yes: The adapter will run the job in the future through perform_later. + # + # (Gem): An additional gem is required to use perform_later with this adapter. + # + # No: The adapter will run jobs at the next opportunity and cannot use perform_later. + # + # N/A: The adapter does not support queueing. # # NOTE: - # queue_classic does not support Job scheduling. However you can implement this - # yourself or you can use the queue_classic-later gem. See the documentation for - # ActiveJob::QueueAdapters::QueueClassicAdapter. + # queue_classic does not support job scheduling. + # However, you can use the queue_classic-later gem. + # See the documentation for ActiveJob::QueueAdapters::QueueClassicAdapter. + # + # ==== Priorities + # + # The order in which jobs are processed can be configured differently depending + # on the adapter. + # + # Job: Any class inheriting from the adapter may set the priority on the job + # object relative to other jobs. + # + # Queue: The adapter can set the priority for job queues, when setting a queue + # with Active Job this will be respected. + # + # Yes: Allows the priority to be set on the job object, at the queue level or + # as default configuration option. # + # No: Does not allow the priority of jobs to be configured. + # + # N/A: The adapter does not support queueing, and therefore sorting them. + # + # ==== Timeout + # + # When a job will stop after the allotted time. + # + # Job: The timeout can be set for each instance of the job class. + # + # Queue: The timeout is set for all jobs on the queue. + # + # Global: The adapter is configured that all jobs have a maximum run time. + # + # N/A: This adapter does not run in a separate process, and therefore timeout + # is unsupported. + # + # ==== Retries + # + # Job: The number of retries can be set per instance of the job class. + # + # Yes: The Number of retries can be configured globally, for each instance or + # on the queue. This adapter may also present failed instances of the job class + # that can be restarted. + # + # Global: The adapter has a global number of retries. + # + # N/A: The adapter does not run in a separate process, and therefore doesn't + # support retries. module QueueAdapters extend ActiveSupport::Autoload @@ -48,5 +111,14 @@ module ActiveJob autoload :SneakersAdapter autoload :SuckerPunchAdapter autoload :TestAdapter + + ADAPTER = 'Adapter'.freeze + private_constant :ADAPTER + + class << self + def lookup(name) + const_get(name.to_s.camelize << ADAPTER) + end + end end end diff --git a/activejob/lib/active_job/queue_adapters/backburner_adapter.rb b/activejob/lib/active_job/queue_adapters/backburner_adapter.rb index 2453d065de..17703e3e41 100644 --- a/activejob/lib/active_job/queue_adapters/backburner_adapter.rb +++ b/activejob/lib/active_job/queue_adapters/backburner_adapter.rb @@ -13,15 +13,13 @@ module ActiveJob # # Rails.application.config.active_job.queue_adapter = :backburner class BackburnerAdapter - class << self - def enqueue(job) #:nodoc: - Backburner::Worker.enqueue JobWrapper, [ job.serialize ], queue: job.queue_name - end + def enqueue(job) #:nodoc: + Backburner::Worker.enqueue JobWrapper, [ job.serialize ], queue: job.queue_name + end - def enqueue_at(job, timestamp) #:nodoc: - delay = timestamp - Time.current.to_f - Backburner::Worker.enqueue JobWrapper, [ job.serialize ], queue: job.queue_name, delay: delay - end + def enqueue_at(job, timestamp) #:nodoc: + delay = timestamp - Time.current.to_f + Backburner::Worker.enqueue JobWrapper, [ job.serialize ], queue: job.queue_name, delay: delay end class JobWrapper #:nodoc: diff --git a/activejob/lib/active_job/queue_adapters/delayed_job_adapter.rb b/activejob/lib/active_job/queue_adapters/delayed_job_adapter.rb index 69d9e70de3..852a6ee326 100644 --- a/activejob/lib/active_job/queue_adapters/delayed_job_adapter.rb +++ b/activejob/lib/active_job/queue_adapters/delayed_job_adapter.rb @@ -13,14 +13,12 @@ module ActiveJob # # Rails.application.config.active_job.queue_adapter = :delayed_job class DelayedJobAdapter - class << self - def enqueue(job) #:nodoc: - Delayed::Job.enqueue(JobWrapper.new(job.serialize), queue: job.queue_name) - end + def enqueue(job) #:nodoc: + Delayed::Job.enqueue(JobWrapper.new(job.serialize), queue: job.queue_name) + end - def enqueue_at(job, timestamp) #:nodoc: - Delayed::Job.enqueue(JobWrapper.new(job.serialize), queue: job.queue_name, run_at: Time.at(timestamp)) - end + def enqueue_at(job, timestamp) #:nodoc: + Delayed::Job.enqueue(JobWrapper.new(job.serialize), queue: job.queue_name, run_at: Time.at(timestamp)) end class JobWrapper #:nodoc: diff --git a/activejob/lib/active_job/queue_adapters/inline_adapter.rb b/activejob/lib/active_job/queue_adapters/inline_adapter.rb index e25d88e723..1d06324c18 100644 --- a/activejob/lib/active_job/queue_adapters/inline_adapter.rb +++ b/activejob/lib/active_job/queue_adapters/inline_adapter.rb @@ -9,14 +9,12 @@ module ActiveJob # # Rails.application.config.active_job.queue_adapter = :inline class InlineAdapter - class << self - def enqueue(job) #:nodoc: - Base.execute(job.serialize) - end + def enqueue(job) #:nodoc: + Base.execute(job.serialize) + end - def enqueue_at(*) #:nodoc: - raise NotImplementedError.new("Use a queueing backend to enqueue jobs in the future. Read more at http://guides.rubyonrails.org/active_job_basics.html") - end + def enqueue_at(*) #:nodoc: + raise NotImplementedError.new("Use a queueing backend to enqueue jobs in the future. Read more at http://guides.rubyonrails.org/active_job_basics.html") end end end diff --git a/activejob/lib/active_job/queue_adapters/qu_adapter.rb b/activejob/lib/active_job/queue_adapters/qu_adapter.rb index 30aa5a4670..94584ef9d8 100644 --- a/activejob/lib/active_job/queue_adapters/qu_adapter.rb +++ b/activejob/lib/active_job/queue_adapters/qu_adapter.rb @@ -16,16 +16,14 @@ module ActiveJob # # Rails.application.config.active_job.queue_adapter = :qu class QuAdapter - class << self - def enqueue(job, *args) #:nodoc: - Qu::Payload.new(klass: JobWrapper, args: [job.serialize]).tap do |payload| - payload.instance_variable_set(:@queue, job.queue_name) - end.push - end + def enqueue(job, *args) #:nodoc: + Qu::Payload.new(klass: JobWrapper, args: [job.serialize]).tap do |payload| + payload.instance_variable_set(:@queue, job.queue_name) + end.push + end - def enqueue_at(job, timestamp, *args) #:nodoc: - raise NotImplementedError - end + def enqueue_at(job, timestamp, *args) #:nodoc: + raise NotImplementedError end class JobWrapper < Qu::Job #:nodoc: diff --git a/activejob/lib/active_job/queue_adapters/que_adapter.rb b/activejob/lib/active_job/queue_adapters/que_adapter.rb index e501fe0368..84cc2845b0 100644 --- a/activejob/lib/active_job/queue_adapters/que_adapter.rb +++ b/activejob/lib/active_job/queue_adapters/que_adapter.rb @@ -15,14 +15,12 @@ module ActiveJob # # Rails.application.config.active_job.queue_adapter = :que class QueAdapter - class << self - def enqueue(job) #:nodoc: - JobWrapper.enqueue job.serialize, queue: job.queue_name - end + def enqueue(job) #:nodoc: + JobWrapper.enqueue job.serialize, queue: job.queue_name + end - def enqueue_at(job, timestamp) #:nodoc: - JobWrapper.enqueue job.serialize, queue: job.queue_name, run_at: Time.at(timestamp) - end + def enqueue_at(job, timestamp) #:nodoc: + JobWrapper.enqueue job.serialize, queue: job.queue_name, run_at: Time.at(timestamp) end class JobWrapper < Que::Job #:nodoc: diff --git a/activejob/lib/active_job/queue_adapters/queue_classic_adapter.rb b/activejob/lib/active_job/queue_adapters/queue_classic_adapter.rb index 34c11a68b2..059754a87f 100644 --- a/activejob/lib/active_job/queue_adapters/queue_classic_adapter.rb +++ b/activejob/lib/active_job/queue_adapters/queue_classic_adapter.rb @@ -17,29 +17,27 @@ module ActiveJob # # Rails.application.config.active_job.queue_adapter = :queue_classic class QueueClassicAdapter - class << self - def enqueue(job) #:nodoc: - build_queue(job.queue_name).enqueue("#{JobWrapper.name}.perform", job.serialize) - end + def enqueue(job) #:nodoc: + build_queue(job.queue_name).enqueue("#{JobWrapper.name}.perform", job.serialize) + end - def enqueue_at(job, timestamp) #:nodoc: - queue = build_queue(job.queue_name) - unless queue.respond_to?(:enqueue_at) - raise NotImplementedError, 'To be able to schedule jobs with queue_classic ' \ - 'the QC::Queue needs to respond to `enqueue_at(timestamp, method, *args)`. ' \ - 'You can implement this yourself or you can use the queue_classic-later gem.' - end - queue.enqueue_at(timestamp, "#{JobWrapper.name}.perform", job.serialize) + def enqueue_at(job, timestamp) #:nodoc: + queue = build_queue(job.queue_name) + unless queue.respond_to?(:enqueue_at) + raise NotImplementedError, 'To be able to schedule jobs with queue_classic ' \ + 'the QC::Queue needs to respond to `enqueue_at(timestamp, method, *args)`. ' \ + 'You can implement this yourself or you can use the queue_classic-later gem.' end + queue.enqueue_at(timestamp, "#{JobWrapper.name}.perform", job.serialize) + end - # Builds a <tt>QC::Queue</tt> object to schedule jobs on. - # - # If you have a custom <tt>QC::Queue</tt> subclass you'll need to subclass - # <tt>ActiveJob::QueueAdapters::QueueClassicAdapter</tt> and override the - # <tt>build_queue</tt> method. - def build_queue(queue_name) - QC::Queue.new(queue_name) - end + # Builds a <tt>QC::Queue</tt> object to schedule jobs on. + # + # If you have a custom <tt>QC::Queue</tt> subclass you'll need to subclass + # <tt>ActiveJob::QueueAdapters::QueueClassicAdapter</tt> and override the + # <tt>build_queue</tt> method. + def build_queue(queue_name) + QC::Queue.new(queue_name) end class JobWrapper #:nodoc: diff --git a/activejob/lib/active_job/queue_adapters/resque_adapter.rb b/activejob/lib/active_job/queue_adapters/resque_adapter.rb index 88c6b48fef..417854afd8 100644 --- a/activejob/lib/active_job/queue_adapters/resque_adapter.rb +++ b/activejob/lib/active_job/queue_adapters/resque_adapter.rb @@ -26,18 +26,16 @@ module ActiveJob # # Rails.application.config.active_job.queue_adapter = :resque class ResqueAdapter - class << self - def enqueue(job) #:nodoc: - Resque.enqueue_to job.queue_name, JobWrapper, job.serialize - end + def enqueue(job) #:nodoc: + Resque.enqueue_to job.queue_name, JobWrapper, job.serialize + end - def enqueue_at(job, timestamp) #:nodoc: - 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.serialize + def enqueue_at(job, timestamp) #:nodoc: + 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.serialize end class JobWrapper #:nodoc: diff --git a/activejob/lib/active_job/queue_adapters/sidekiq_adapter.rb b/activejob/lib/active_job/queue_adapters/sidekiq_adapter.rb index 21005fc728..743d5ea333 100644 --- a/activejob/lib/active_job/queue_adapters/sidekiq_adapter.rb +++ b/activejob/lib/active_job/queue_adapters/sidekiq_adapter.rb @@ -15,22 +15,22 @@ module ActiveJob # # Rails.application.config.active_job.queue_adapter = :sidekiq class SidekiqAdapter - class << self - def enqueue(job) #:nodoc: - #Sidekiq::Client does not support symbols as keys - Sidekiq::Client.push \ - 'class' => JobWrapper, - 'queue' => job.queue_name, - 'args' => [ job.serialize ] - end + def enqueue(job) #:nodoc: + #Sidekiq::Client does not support symbols as keys + Sidekiq::Client.push \ + 'class' => JobWrapper, + 'wrapped' => job.class.to_s, + 'queue' => job.queue_name, + 'args' => [ job.serialize ] + end - def enqueue_at(job, timestamp) #:nodoc: - Sidekiq::Client.push \ - 'class' => JobWrapper, - 'queue' => job.queue_name, - 'args' => [ job.serialize ], - 'at' => timestamp - end + def enqueue_at(job, timestamp) #:nodoc: + Sidekiq::Client.push \ + 'class' => JobWrapper, + 'wrapped' => job.class.to_s, + 'queue' => job.queue_name, + 'args' => [ job.serialize ], + 'at' => timestamp end class JobWrapper #:nodoc: diff --git a/activejob/lib/active_job/queue_adapters/sneakers_adapter.rb b/activejob/lib/active_job/queue_adapters/sneakers_adapter.rb index 6d60a2f303..f5737487ca 100644 --- a/activejob/lib/active_job/queue_adapters/sneakers_adapter.rb +++ b/activejob/lib/active_job/queue_adapters/sneakers_adapter.rb @@ -16,19 +16,19 @@ module ActiveJob # # Rails.application.config.active_job.queue_adapter = :sneakers class SneakersAdapter - @monitor = Monitor.new + def initialize + @monitor = Monitor.new + end - class << self - def enqueue(job) #:nodoc: - @monitor.synchronize do - JobWrapper.from_queue job.queue_name - JobWrapper.enqueue ActiveSupport::JSON.encode(job.serialize) - end + def enqueue(job) #:nodoc: + @monitor.synchronize do + JobWrapper.from_queue job.queue_name + JobWrapper.enqueue ActiveSupport::JSON.encode(job.serialize) end + end - def enqueue_at(job, timestamp) #:nodoc: - raise NotImplementedError - end + def enqueue_at(job, timestamp) #:nodoc: + raise NotImplementedError end class JobWrapper #:nodoc: diff --git a/activejob/lib/active_job/queue_adapters/sucker_punch_adapter.rb b/activejob/lib/active_job/queue_adapters/sucker_punch_adapter.rb index be9e7fd03a..64c93e8198 100644 --- a/activejob/lib/active_job/queue_adapters/sucker_punch_adapter.rb +++ b/activejob/lib/active_job/queue_adapters/sucker_punch_adapter.rb @@ -18,14 +18,12 @@ module ActiveJob # # Rails.application.config.active_job.queue_adapter = :sucker_punch class SuckerPunchAdapter - class << self - def enqueue(job) #:nodoc: - JobWrapper.new.async.perform job.serialize - end + def enqueue(job) #:nodoc: + JobWrapper.new.async.perform job.serialize + end - def enqueue_at(job, timestamp) #:nodoc: - raise NotImplementedError - end + def enqueue_at(job, timestamp) #:nodoc: + raise NotImplementedError end class JobWrapper #:nodoc: diff --git a/activejob/lib/active_job/queue_adapters/test_adapter.rb b/activejob/lib/active_job/queue_adapters/test_adapter.rb index fd7c0b207a..9b7b7139f4 100644 --- a/activejob/lib/active_job/queue_adapters/test_adapter.rb +++ b/activejob/lib/active_job/queue_adapters/test_adapter.rb @@ -10,52 +10,50 @@ module ActiveJob # # Rails.application.config.active_job.queue_adapter = :test class TestAdapter - class << self - attr_accessor(:perform_enqueued_jobs, :perform_enqueued_at_jobs, :filter) - attr_writer(:enqueued_jobs, :performed_jobs) + attr_accessor(:perform_enqueued_jobs, :perform_enqueued_at_jobs, :filter) + attr_writer(:enqueued_jobs, :performed_jobs) - # Provides a store of all the enqueued jobs with the TestAdapter so you can check them. - def enqueued_jobs - @enqueued_jobs ||= [] - end + # Provides a store of all the enqueued jobs with the TestAdapter so you can check them. + def enqueued_jobs + @enqueued_jobs ||= [] + end - # Provides a store of all the performed jobs with the TestAdapter so you can check them. - def performed_jobs - @performed_jobs ||= [] - end + # Provides a store of all the performed jobs with the TestAdapter so you can check them. + def performed_jobs + @performed_jobs ||= [] + end - def enqueue(job) #:nodoc: - return if filtered?(job) + def enqueue(job) #:nodoc: + return if filtered?(job) - job_data = job_to_hash(job) - enqueue_or_perform(perform_enqueued_jobs, job, job_data) - end + job_data = job_to_hash(job) + enqueue_or_perform(perform_enqueued_jobs, job, job_data) + end - def enqueue_at(job, timestamp) #:nodoc: - return if filtered?(job) + def enqueue_at(job, timestamp) #:nodoc: + return if filtered?(job) - job_data = job_to_hash(job, at: timestamp) - enqueue_or_perform(perform_enqueued_at_jobs, job, job_data) - end + job_data = job_to_hash(job, at: timestamp) + enqueue_or_perform(perform_enqueued_at_jobs, job, job_data) + end - private + private - def job_to_hash(job, extras = {}) - { job: job.class, args: job.serialize.fetch('arguments'), queue: job.queue_name }.merge!(extras) - end + def job_to_hash(job, extras = {}) + { job: job.class, args: job.serialize.fetch('arguments'), queue: job.queue_name }.merge!(extras) + end - def enqueue_or_perform(perform, job, job_data) - if perform - performed_jobs << job_data - Base.execute job.serialize - else - enqueued_jobs << job_data - end + def enqueue_or_perform(perform, job, job_data) + if perform + performed_jobs << job_data + Base.execute job.serialize + else + enqueued_jobs << job_data end + end - def filtered?(job) - filter && !Array(filter).include?(job.class) - end + def filtered?(job) + filter && !Array(filter).include?(job.class) end end end diff --git a/activejob/lib/active_job/test_helper.rb b/activejob/lib/active_job/test_helper.rb index bb8b267c31..4efb4b72d2 100644 --- a/activejob/lib/active_job/test_helper.rb +++ b/activejob/lib/active_job/test_helper.rb @@ -1,3 +1,4 @@ +require 'active_support/core_ext/class/subclasses' require 'active_support/core_ext/hash/keys' module ActiveJob @@ -7,19 +8,27 @@ module ActiveJob included do def before_setup - @old_queue_adapter = queue_adapter - ActiveJob::Base.queue_adapter = :test + test_adapter = ActiveJob::QueueAdapters::TestAdapter.new + + @old_queue_adapters = (ActiveJob::Base.subclasses << ActiveJob::Base).select do |klass| + # only override explicitly set adapters, a quirk of `class_attribute` + klass.singleton_class.public_instance_methods(false).include?(:_queue_adapter) + end.map do |klass| + [klass, klass.queue_adapter].tap do + klass.queue_adapter = test_adapter + end + end + clear_enqueued_jobs clear_performed_jobs - queue_adapter.perform_enqueued_jobs = false - queue_adapter.perform_enqueued_at_jobs = false - queue_adapter.filter = nil super end def after_teardown super - ActiveJob::Base.queue_adapter = @old_queue_adapter + @old_queue_adapters.each do |(klass, adapter)| + klass.queue_adapter = adapter + end end # Asserts that the number of enqueued jobs matches the given number. diff --git a/activejob/lib/rails/generators/job/job_generator.rb b/activejob/lib/rails/generators/job/job_generator.rb index 979ffcb748..86e4c5266c 100644 --- a/activejob/lib/rails/generators/job/job_generator.rb +++ b/activejob/lib/rails/generators/job/job_generator.rb @@ -18,7 +18,6 @@ module Rails 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 index 462c71d917..4ad2914a45 100644 --- a/activejob/lib/rails/generators/job/templates/job.rb +++ b/activejob/lib/rails/generators/job/templates/job.rb @@ -1,5 +1,5 @@ <% module_namespacing do -%> -class <%= class_name %>Job < ActiveJob::Base +class <%= class_name %>Job < ApplicationJob queue_as :<%= options[:queue] %> def perform(*args) diff --git a/activejob/test/cases/adapter_test.rb b/activejob/test/cases/adapter_test.rb index f0c710f9ed..6d75ae9a7c 100644 --- a/activejob/test/cases/adapter_test.rb +++ b/activejob/test/cases/adapter_test.rb @@ -2,6 +2,6 @@ require 'helper' class AdapterTest < ActiveSupport::TestCase test "should load #{ENV['AJ_ADAPTER']} adapter" do - assert_equal "active_job/queue_adapters/#{ENV['AJ_ADAPTER']}_adapter".classify, ActiveJob::Base.queue_adapter.name + assert_equal "active_job/queue_adapters/#{ENV['AJ_ADAPTER']}_adapter".classify, ActiveJob::Base.queue_adapter.class.name end end diff --git a/activejob/test/cases/logging_test.rb b/activejob/test/cases/logging_test.rb index 64aae00441..b18be553ec 100644 --- a/activejob/test/cases/logging_test.rb +++ b/activejob/test/cases/logging_test.rb @@ -6,7 +6,7 @@ require 'jobs/logging_job' require 'jobs/nested_job' require 'models/person' -class AdapterTest < ActiveSupport::TestCase +class LoggingTest < ActiveSupport::TestCase include ActiveSupport::LogSubscriber::TestHelper include ActiveSupport::Logger::Severity diff --git a/activejob/test/cases/queue_adapter_test.rb b/activejob/test/cases/queue_adapter_test.rb new file mode 100644 index 0000000000..fb3fdc392f --- /dev/null +++ b/activejob/test/cases/queue_adapter_test.rb @@ -0,0 +1,56 @@ +require 'helper' + +module ActiveJob + module QueueAdapters + class StubOneAdapter + def enqueue(*); end + def enqueue_at(*); end + end + + class StubTwoAdapter + def enqueue(*); end + def enqueue_at(*); end + end + end +end + +class QueueAdapterTest < ActiveJob::TestCase + test 'should forbid nonsense arguments' do + assert_raises(ArgumentError) { ActiveJob::Base.queue_adapter = Mutex } + assert_raises(ArgumentError) { ActiveJob::Base.queue_adapter = Mutex.new } + end + + test 'should warn on passing an adapter class' do + klass = Class.new do + def self.name + 'fake' + end + + def enqueue(*); end + def enqueue_at(*); end + end + + assert_deprecated { ActiveJob::Base.queue_adapter = klass } + end + + test 'should allow overriding the queue_adapter at the child class level without affecting the parent or its sibling' do + base_queue_adapter = ActiveJob::Base.queue_adapter + + child_job_one = Class.new(ActiveJob::Base) + child_job_one.queue_adapter = :stub_one + + assert_not_equal ActiveJob::Base.queue_adapter, child_job_one.queue_adapter + assert_kind_of ActiveJob::QueueAdapters::StubOneAdapter, child_job_one.queue_adapter + + child_job_two = Class.new(ActiveJob::Base) + child_job_two.queue_adapter = :stub_two + + assert_kind_of ActiveJob::QueueAdapters::StubTwoAdapter, child_job_two.queue_adapter + assert_kind_of ActiveJob::QueueAdapters::StubOneAdapter, child_job_one.queue_adapter, "child_job_one's queue adapter should remain unchanged" + assert_equal base_queue_adapter, ActiveJob::Base.queue_adapter, "ActiveJob::Base's queue adapter should remain unchanged" + + child_job_three = Class.new(ActiveJob::Base) + + assert_not_nil child_job_three.queue_adapter + end +end diff --git a/activejob/test/cases/test_case_test.rb b/activejob/test/cases/test_case_test.rb index 7d1702990e..0a3a20d5a0 100644 --- a/activejob/test/cases/test_case_test.rb +++ b/activejob/test/cases/test_case_test.rb @@ -4,11 +4,20 @@ require 'jobs/logging_job' require 'jobs/nested_job' class ActiveJobTestCaseTest < ActiveJob::TestCase + # this tests that this job class doesn't get its adapter set. + # that's the correct behaviour since we don't want to break + # the `class_attribute` inheritence + class TestClassAttributeInheritenceJob < ActiveJob::Base + def self.queue_adapter=(*) + raise 'Attemping to break `class_attribute` inheritence, bad!' + end + end + def test_include_helper assert_includes self.class.ancestors, ActiveJob::TestHelper end def test_set_test_adapter - assert_equal ActiveJob::QueueAdapters::TestAdapter, self.queue_adapter + assert_kind_of ActiveJob::QueueAdapters::TestAdapter, self.queue_adapter end end diff --git a/activejob/test/integration/queuing_test.rb b/activejob/test/integration/queuing_test.rb index 38874b51a8..09f5c329cc 100644 --- a/activejob/test/integration/queuing_test.rb +++ b/activejob/test/integration/queuing_test.rb @@ -1,5 +1,6 @@ require 'helper' require 'jobs/logging_job' +require 'jobs/hello_job' require 'active_support/core_ext/numeric/time' class QueuingTest < ActiveSupport::TestCase @@ -23,6 +24,16 @@ class QueuingTest < ActiveSupport::TestCase end end + test 'should supply a wrapped class name to Sidekiq' do + skip unless adapter_is?(:sidekiq) + Sidekiq::Testing.fake! do + ::HelloJob.perform_later + hash = ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper.jobs.first + assert_equal "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper", hash['class'] + assert_equal "HelloJob", hash['wrapped'] + end + end + test 'should not run job enqueued in the future' do begin TestJob.set(wait: 10.minutes).perform_later @id @@ -35,7 +46,7 @@ class QueuingTest < ActiveSupport::TestCase test 'should run job enqueued in the future at the specified time' do begin - TestJob.set(wait: 3.seconds).perform_later @id + TestJob.set(wait: 5.seconds).perform_later @id wait_for_jobs_to_finish_for(2.seconds) assert_not job_executed wait_for_jobs_to_finish_for(10.seconds) diff --git a/activejob/test/support/integration/adapters/que.rb b/activejob/test/support/integration/adapters/que.rb index ba7657a42a..ff5235bdc8 100644 --- a/activejob/test/support/integration/adapters/que.rb +++ b/activejob/test/support/integration/adapters/que.rb @@ -2,6 +2,15 @@ module QueJobsManager def setup require 'sequel' ActiveJob::Base.queue_adapter = :que + Que.mode = :off + Que.worker_count = 1 + end + + def clear_jobs + Que.clear! + end + + def start_workers que_url = ENV['QUE_DATABASE_URL'] || 'postgres:///active_jobs_que_int_test' uri = URI.parse(que_url) user = uri.user||ENV['USER'] @@ -11,24 +20,17 @@ module QueJobsManager %x{#{"PGPASSWORD=\"#{pass}\"" if pass} psql -c 'create database "#{db}"' -U #{user} -t template1} Que.connection = Sequel.connect(que_url) Que.migrate! - Que.mode = :off - Que.worker_count = 1 - rescue Sequel::DatabaseConnectionError - puts "Cannot run integration tests for que. To be able to run integration tests for que you need to install and start postgresql.\n" - exit - end - - def clear_jobs - Que.clear! - end - def start_workers @thread = Thread.new do loop do Que::Job.work("integration_tests") sleep 0.5 end end + + rescue Sequel::DatabaseConnectionError + puts "Cannot run integration tests for que. To be able to run integration tests for que you need to install and start postgresql.\n" + exit end def stop_workers diff --git a/activejob/test/support/integration/adapters/queue_classic.rb b/activejob/test/support/integration/adapters/queue_classic.rb index f522b2711f..29c04bf625 100644 --- a/activejob/test/support/integration/adapters/queue_classic.rb +++ b/activejob/test/support/integration/adapters/queue_classic.rb @@ -3,17 +3,7 @@ module QueueClassicJobsManager ENV['QC_DATABASE_URL'] ||= 'postgres:///active_jobs_qc_int_test' ENV['QC_RAILS_DATABASE'] = 'false' ENV['QC_LISTEN_TIME'] = "0.5" - uri = URI.parse(ENV['QC_DATABASE_URL']) - user = uri.user||ENV['USER'] - pass = uri.password - db = uri.path[1..-1] - %x{#{"PGPASSWORD=\"#{pass}\"" if pass} psql -c 'drop database if exists "#{db}"' -U #{user} -t template1} - %x{#{"PGPASSWORD=\"#{pass}\"" if pass} psql -c 'create database "#{db}"' -U #{user} -t template1} ActiveJob::Base.queue_adapter = :queue_classic - QC::Setup.create - rescue PG::ConnectionBad - puts "Cannot run integration tests for queue_classic. To be able to run integration tests for queue_classic you need to install and start postgresql.\n" - exit end def clear_jobs @@ -21,12 +11,24 @@ module QueueClassicJobsManager end def start_workers + uri = URI.parse(ENV['QC_DATABASE_URL']) + user = uri.user||ENV['USER'] + pass = uri.password + db = uri.path[1..-1] + %x{#{"PGPASSWORD=\"#{pass}\"" if pass} psql -c 'drop database if exists "#{db}"' -U #{user} -t template1} + %x{#{"PGPASSWORD=\"#{pass}\"" if pass} psql -c 'create database "#{db}"' -U #{user} -t template1} + QC::Setup.create + QC.default_conn_adapter.disconnect QC.default_conn_adapter = nil @pid = fork do worker = QC::Worker.new(q_name: 'integration_tests') worker.start end + + rescue PG::ConnectionBad + puts "Cannot run integration tests for queue_classic. To be able to run integration tests for queue_classic you need to install and start postgresql.\n" + exit end def stop_workers diff --git a/activejob/test/support/integration/adapters/sidekiq.rb b/activejob/test/support/integration/adapters/sidekiq.rb index 6ff18fb56a..4988cdb33f 100644 --- a/activejob/test/support/integration/adapters/sidekiq.rb +++ b/activejob/test/support/integration/adapters/sidekiq.rb @@ -1,6 +1,8 @@ -require 'sidekiq/cli' require 'sidekiq/api' +require 'sidekiq/testing' +Sidekiq::Testing.disable! + module SidekiqJobsManager def setup @@ -17,33 +19,71 @@ module SidekiqJobsManager end def start_workers - fork do - sidekiq = Sidekiq::CLI.instance + continue_read, continue_write = IO.pipe + death_read, death_write = IO.pipe + + @pid = fork do + continue_read.close + death_write.close + + # Celluloid & Sidekiq are not warning-clean :( + $VERBOSE = false + + $stdin.reopen('/dev/null') + $stdout.sync = true + $stderr.sync = true + logfile = Rails.root.join("log/sidekiq.log").to_s - pidfile = Rails.root.join("tmp/sidekiq.pid").to_s - sidekiq.parse([ "--require", Rails.root.to_s, - "--queue", "integration_tests", - "--logfile", logfile, - "--pidfile", pidfile, - "--environment", "test", - "--concurrency", "1", - "--timeout", "1", - "--daemon", - ]) + Sidekiq::Logging.initialize_logger(logfile) + + self_read, self_write = IO.pipe + trap "TERM" do + self_write.puts("TERM") + end + + Thread.new do + begin + death_read.read + rescue Exception + end + self_write.puts("TERM") + end + require 'celluloid' - require 'sidekiq/scheduled' + Celluloid.logger = nil + require 'sidekiq/launcher' + sidekiq = Sidekiq::Launcher.new({queues: ["integration_tests"], + environment: "test", + concurrency: 1, + timeout: 1, + }) Sidekiq.poll_interval = 0.5 Sidekiq::Scheduled.const_set :INITIAL_WAIT, 1 - sidekiq.run + begin + sidekiq.run + continue_write.puts "started" + while readable_io = IO.select([self_read]) + signal = readable_io.first[0].gets.strip + raise Interrupt if signal == "TERM" + end + rescue Interrupt + end + + sidekiq.stop + exit! end - sleep 1 + continue_write.close + death_read.close + @worker_lifeline = death_write + + raise "Failed to start worker" unless continue_read.gets == "started\n" end def stop_workers - pidfile = Rails.root.join("tmp/sidekiq.pid").to_s - Process.kill 'TERM', File.open(pidfile).read.to_i - FileUtils.rm_f pidfile - rescue + if @pid + Process.kill 'TERM', @pid + Process.wait @pid + end end def can_run? diff --git a/activejob/test/support/integration/test_case_helpers.rb b/activejob/test/support/integration/test_case_helpers.rb index ee2f6aebea..bed28b2900 100644 --- a/activejob/test/support/integration/test_case_helpers.rb +++ b/activejob/test/support/integration/test_case_helpers.rb @@ -5,7 +5,7 @@ module TestCaseHelpers extend ActiveSupport::Concern included do - self.use_transactional_fixtures = false + self.use_transactional_tests = false setup do clear_jobs @@ -27,8 +27,8 @@ module TestCaseHelpers jobs_manager.clear_jobs end - def adapter_is?(adapter) - ActiveJob::Base.queue_adapter.name.split("::").last.gsub(/Adapter$/, '').underscore==adapter.to_s + def adapter_is?(adapter_class_symbol) + ActiveJob::Base.queue_adapter.class.name.split("::").last.gsub(/Adapter$/, '').underscore == adapter_class_symbol.to_s end def wait_for_jobs_to_finish_for(seconds=60) diff --git a/activemodel/CHANGELOG.md b/activemodel/CHANGELOG.md index 80b42859e2..4961d69d2a 100644 --- a/activemodel/CHANGELOG.md +++ b/activemodel/CHANGELOG.md @@ -1,3 +1,17 @@ +* Add `ActiveModel::Dirty#[attr_name]_previously_changed?` and + `ActiveModel::Dirty#[attr_name]_previous_change` to improve access + to recorded changes after the model has been saved. + + It makes the dirty-attributes query methods consistent before and after + saving. + + *Fernando Tapia Rico* + +* Deprecate the `:tokenizer` option for `validates_length_of`, in favor of + plain Ruby. + + *Sean Griffin* + * Deprecate `ActiveModel::Errors#add_on_empty` and `ActiveModel::Errors#add_on_blank` with no replacement. @@ -8,7 +22,7 @@ *Wojciech Wnętrzak* -* Allow symbol as values for `tokenize` of `LengthValidator` +* Allow symbol as values for `tokenize` of `LengthValidator`. *Kensuke Naito* @@ -35,11 +49,11 @@ cat = Cat.new cat.assign_attributes(name: "Gorby", status: "yawning") - cat.name # => 'Gorby' - cat.status => 'yawning' + cat.name # => 'Gorby' + cat.status # => 'yawning' cat.assign_attributes(status: "sleeping") - cat.name # => 'Gorby' - cat.status => 'sleeping' + cat.name # => 'Gorby' + cat.status # => 'sleeping' *Bogdan Gusiev* diff --git a/activemodel/activemodel.gemspec b/activemodel/activemodel.gemspec index 3c6eb56296..8d00b3aa27 100644 --- a/activemodel/activemodel.gemspec +++ b/activemodel/activemodel.gemspec @@ -7,7 +7,7 @@ Gem::Specification.new do |s| s.summary = 'A toolkit for building modeling frameworks (part of Rails).' s.description = 'A toolkit for building modeling frameworks like Active Record. Rich support for attributes, callbacks, validations, serialization, internationalization, and testing.' - s.required_ruby_version = '>= 2.2.0' + s.required_ruby_version = '>= 2.2.2' s.license = 'MIT' diff --git a/activemodel/lib/active_model/attribute_methods.rb b/activemodel/lib/active_model/attribute_methods.rb index 96be551264..ff7280f9e5 100644 --- a/activemodel/lib/active_model/attribute_methods.rb +++ b/activemodel/lib/active_model/attribute_methods.rb @@ -23,7 +23,7 @@ module ActiveModel # The requirements to implement <tt>ActiveModel::AttributeMethods</tt> are to: # # * <tt>include ActiveModel::AttributeMethods</tt> in your class. - # * Call each of its method you want to add, such as +attribute_method_suffix+ + # * Call each of its methods you want to add, such as +attribute_method_suffix+ # or +attribute_method_prefix+. # * Call +define_attribute_methods+ after the other methods are called. # * Define the various generic +_attribute+ methods that you have declared. @@ -227,7 +227,7 @@ module ActiveModel # Declares the attributes that should be prefixed and suffixed by # ActiveModel::AttributeMethods. # - # To use, pass attribute names (as strings or symbols), be sure to declare + # To use, pass attribute names (as strings or symbols). Be sure to declare # +define_attribute_methods+ after you define any prefix, suffix or affix # methods, or they will not hook in. # @@ -239,7 +239,7 @@ module ActiveModel # # # Call to define_attribute_methods must appear after the # # attribute_method_prefix, attribute_method_suffix or - # # attribute_method_affix declares. + # # attribute_method_affix declarations. # define_attribute_methods :name, :age, :address # # private @@ -255,7 +255,7 @@ module ActiveModel # Declares an attribute that should be prefixed and suffixed by # ActiveModel::AttributeMethods. # - # To use, pass an attribute name (as string or symbol), be sure to declare + # To use, pass an attribute name (as string or symbol). Be sure to declare # +define_attribute_method+ after you define any prefix, suffix or affix # method, or they will not hook in. # @@ -267,7 +267,7 @@ module ActiveModel # # # Call to define_attribute_method must appear after the # # attribute_method_prefix, attribute_method_suffix or - # # attribute_method_affix declares. + # # attribute_method_affix declarations. # define_attribute_method :name # # private @@ -363,7 +363,7 @@ module ActiveModel end # Define a method `name` in `mod` that dispatches to `send` - # using the given `extra` args. This fallbacks `define_method` + # using the given `extra` args. This falls back on `define_method` # and `send` if the given names cannot be compiled. def define_proxy_call(include_private, mod, name, send, *extra) #:nodoc: defn = if name =~ NAME_COMPILABLE_REGEXP @@ -419,7 +419,7 @@ module ActiveModel # returned by <tt>attributes</tt>, as though they were first-class # methods. So a +Person+ class with a +name+ attribute can for example use # <tt>Person#name</tt> and <tt>Person#name=</tt> and never directly use - # the attributes hash -- except for multiple assigns with + # the attributes hash -- except for multiple assignments with # <tt>ActiveRecord::Base#attributes=</tt>. # # It's also possible to instantiate related objects, so a <tt>Client</tt> diff --git a/activemodel/lib/active_model/dirty.rb b/activemodel/lib/active_model/dirty.rb index c03e5fac79..c0fc507286 100644 --- a/activemodel/lib/active_model/dirty.rb +++ b/activemodel/lib/active_model/dirty.rb @@ -76,9 +76,11 @@ module ActiveModel # # Reset the changes: # - # person.previous_changes # => {"name" => ["Uncle Bob", "Bill"]} + # person.previous_changes # => {"name" => ["Uncle Bob", "Bill"]} + # person.name_previously_changed? # => true + # person.name_previous_change # => ["Uncle Bob", "Bill"] # person.reload! - # person.previous_changes # => {} + # person.previous_changes # => {} # # Rollback the changes: # @@ -115,6 +117,7 @@ module ActiveModel included do attribute_method_suffix '_changed?', '_change', '_will_change!', '_was' + attribute_method_suffix '_previously_changed?', '_previous_change' attribute_method_affix prefix: 'restore_', suffix: '!' end @@ -179,6 +182,11 @@ module ActiveModel attribute_changed?(attr) ? changed_attributes[attr] : __send__(attr) end + # Handles <tt>*_previously_changed?</tt> for +method_missing+. + def attribute_previously_changed?(attr, options = {}) #:nodoc: + previous_changes_include?(attr) + end + # Restore all previous data of the provided attributes. def restore_attributes(attributes = changed) attributes.each { |attr| restore_attribute! attr } @@ -192,6 +200,12 @@ module ActiveModel end alias attribute_changed_by_setter? changes_include? + # Returns +true+ if attr_name were changed before the model was saved, + # +false+ otherwise. + def previous_changes_include?(attr_name) + @previously_changed.include?(attr_name) + end + # Removes current changes and makes them accessible through +previous_changes+. def changes_applied # :doc: @previously_changed = changes @@ -209,6 +223,11 @@ module ActiveModel [changed_attributes[attr], __send__(attr)] if attribute_changed?(attr) end + # Handles <tt>*_previous_change</tt> for +method_missing+. + def attribute_previous_change(attr) + @previously_changed[attr] if attribute_previously_changed?(attr) + end + # Handles <tt>*_will_change!</tt> for +method_missing+. def attribute_will_change!(attr) return if attribute_changed?(attr) diff --git a/activemodel/lib/active_model/errors.rb b/activemodel/lib/active_model/errors.rb index f324788979..f843b279ce 100644 --- a/activemodel/lib/active_model/errors.rb +++ b/activemodel/lib/active_model/errors.rb @@ -3,6 +3,7 @@ require 'active_support/core_ext/array/conversions' require 'active_support/core_ext/string/inflections' require 'active_support/core_ext/object/deep_dup' +require 'active_support/core_ext/string/filters' module ActiveModel # == Active \Model \Errors @@ -33,11 +34,11 @@ module ActiveModel # send(attr) # end # - # def Person.human_attribute_name(attr, options = {}) + # def self.human_attribute_name(attr, options = {}) # attr # end # - # def Person.lookup_ancestors + # def self.lookup_ancestors # [self] # end # end @@ -101,9 +102,7 @@ module ActiveModel def include?(attribute) messages[attribute].present? end - # aliases include? alias :has_key? :include? - # aliases include? alias :key? :include? # Get messages for +key+. @@ -123,9 +122,9 @@ module ActiveModel # Set messages for +key+ to +value+. # - # person.errors.get(:name) # => ["cannot be nil"] + # person.errors[:name] # => ["cannot be nil"] # person.errors.set(:name, ["can't be nil"]) - # person.errors.get(:name) # => ["can't be nil"] + # person.errors[:name] # => ["can't be nil"] def set(key, value) ActiveSupport::Deprecation.warn(<<-MESSAGE.squish) ActiveModel::Errors#set is deprecated and will be removed in Rails 5.1. @@ -138,12 +137,12 @@ module ActiveModel # Delete messages for +key+. Returns the deleted messages. # - # person.errors.get(:name) # => ["cannot be nil"] + # person.errors[:name] # => ["cannot be nil"] # person.errors.delete(:name) # => ["cannot be nil"] - # person.errors.get(:name) # => [] + # person.errors[:name] # => [] def delete(key) - messages.delete(key) details.delete(key) + messages.delete(key) end # When passed a symbol or a name of a method, returns an array of errors @@ -198,6 +197,7 @@ module ActiveModel def size values.flatten.size end + alias :count :size # Returns all message values. # @@ -215,35 +215,15 @@ module ActiveModel messages.keys end - # Returns an array of error messages, with the attribute name included. - # - # person.errors.add(:name, :blank, message: "can't be blank") - # person.errors.add(:name, :not_specified, message: "must be specified") - # person.errors.to_a # => ["name can't be blank", "name must be specified"] - def to_a - full_messages - end - - # Returns the number of error messages. - # - # person.errors.add(:name, :blank, message: "can't be blank") - # person.errors.count # => 1 - # person.errors.add(:name, :not_specified, message: "must be specified") - # person.errors.count # => 2 - def count - to_a.size - end - # Returns +true+ if no errors are found, +false+ otherwise. # If the error message is a string it can be empty. # # person.errors.full_messages # => ["name cannot be nil"] # person.errors.empty? # => false def empty? - all? { |k, v| v && v.empty? && !v.is_a?(String) } + size.zero? end - # aliases empty? - alias_method :blank?, :empty? + alias :blank? :empty? # Returns an xml formatted representation of the Errors hash. # @@ -406,6 +386,7 @@ module ActiveModel def full_messages map { |attribute, message| full_message(attribute, message) } end + alias :to_a :full_messages # Returns all the full error messages for a given attribute in an array. # diff --git a/activemodel/lib/active_model/naming.rb b/activemodel/lib/active_model/naming.rb index 22010b517c..1f1749af4e 100644 --- a/activemodel/lib/active_model/naming.rb +++ b/activemodel/lib/active_model/naming.rb @@ -1,6 +1,7 @@ require 'active_support/core_ext/hash/except' require 'active_support/core_ext/module/introspection' require 'active_support/core_ext/module/remove_method' +require 'active_support/core_ext/module/delegation' module ActiveModel class Name diff --git a/activemodel/lib/active_model/validations.rb b/activemodel/lib/active_model/validations.rb index 176d4c0607..74d60327d6 100644 --- a/activemodel/lib/active_model/validations.rb +++ b/activemodel/lib/active_model/validations.rb @@ -401,7 +401,7 @@ module ActiveModel protected def run_validations! #:nodoc: - _run_validate_callbacks + run_callbacks :validate errors.empty? end diff --git a/activemodel/lib/active_model/validations/callbacks.rb b/activemodel/lib/active_model/validations/callbacks.rb index 4b58ef66e3..b4301c23e4 100644 --- a/activemodel/lib/active_model/validations/callbacks.rb +++ b/activemodel/lib/active_model/validations/callbacks.rb @@ -109,7 +109,7 @@ module ActiveModel # Overwrite run validations to include callbacks. def run_validations! #:nodoc: - _run_validation_callbacks { super } + run_callbacks(:validation) { super } end end end diff --git a/activemodel/lib/active_model/validations/length.rb b/activemodel/lib/active_model/validations/length.rb index 23201b264a..c22a58f9e1 100644 --- a/activemodel/lib/active_model/validations/length.rb +++ b/activemodel/lib/active_model/validations/length.rb @@ -1,3 +1,5 @@ +require "active_support/core_ext/string/strip" + module ActiveModel # == Active \Model Length Validator @@ -18,6 +20,27 @@ module ActiveModel options[:minimum] = 1 end + if options[:tokenizer] + ActiveSupport::Deprecation.warn(<<-EOS.strip_heredoc) + The `:tokenizer` option is deprecated, and will be removed in Rails 5.1. + You can achieve the same functionality by defining an instance method + with the value that you want to validate the length of. For example, + + validates_length_of :essay, minimum: 100, + tokenizer: ->(str) { str.scan(/\w+/) } + + should be written as + + validates_length_of :words_in_essay, minimum: 100 + + private + + def words_in_essay + essay.scan(/\w+/) + end + EOS + end + super end @@ -88,8 +111,13 @@ module ActiveModel # validates_length_of :user_name, within: 6..20, too_long: 'pick a shorter name', too_short: 'pick a longer name' # validates_length_of :zip_code, minimum: 5, too_short: 'please enter at least 5 characters' # validates_length_of :smurf_leader, is: 4, message: "papa is spelled with 4 characters... don't play me." - # validates_length_of :essay, minimum: 100, too_short: 'Your essay must be at least 100 words.', - # tokenizer: ->(str) { str.scan(/\w+/) } + # validates_length_of :words_in_essay, minimum: 100, too_short: 'Your essay must be at least 100 words.' + # + # private + # + # def words_in_essay + # essay.scan(/\w+/) + # end # end # # Configuration options: @@ -112,12 +140,6 @@ module ActiveModel # * <tt>:message</tt> - The error message to use for a <tt>:minimum</tt>, # <tt>:maximum</tt>, or <tt>:is</tt> violation. An alias of the appropriate # <tt>too_long</tt>/<tt>too_short</tt>/<tt>wrong_length</tt> message. - # * <tt>:tokenizer</tt> - A method (as a symbol), proc or string to - # specify how to split up the attribute string. (e.g. - # <tt>tokenizer: :word_tokenizer</tt> to call the +word_tokenizer+ method - # or <tt>tokenizer: ->(str) { str.scan(/\w+/) }</tt> to count words as in - # above example). Defaults to <tt>->(value) { value.split(//) }</tt> which - # counts individual characters. # # There is also a list of default options supported by every validator: # +:if+, +:unless+, +:on+ and +:strict+. diff --git a/activemodel/test/cases/dirty_test.rb b/activemodel/test/cases/dirty_test.rb index 66ed8a350a..d17a12ad12 100644 --- a/activemodel/test/cases/dirty_test.rb +++ b/activemodel/test/cases/dirty_test.rb @@ -137,6 +137,19 @@ class DirtyTest < ActiveModel::TestCase assert_equal [nil, "Jericho Cane"], @model.previous_changes['name'] end + test "setting new attributes should not affect previous changes" do + @model.name = "Jericho Cane" + @model.save + @model.name = "DudeFella ManGuy" + assert_equal [nil, "Jericho Cane"], @model.name_previous_change + end + + test "saving should preserve model's previous changed status" do + @model.name = "Jericho Cane" + @model.save + assert @model.name_previously_changed? + end + test "previous value is preserved when changed after save" do assert_equal({}, @model.changed_attributes) @model.name = "Paul" diff --git a/activemodel/test/cases/errors_test.rb b/activemodel/test/cases/errors_test.rb index da142ea2c0..f781a0017f 100644 --- a/activemodel/test/cases/errors_test.rb +++ b/activemodel/test/cases/errors_test.rb @@ -212,6 +212,12 @@ class ErrorsTest < ActiveModel::TestCase assert_equal 1, person.errors.size end + test "count calculates the number of error messages" do + person = Person.new + person.errors.add(:name, "cannot be blank") + assert_equal 1, person.errors.count + end + test "to_a returns the list of errors with complete messages containing the attribute names" do person = Person.new person.errors.add(:name, "cannot be blank") @@ -377,6 +383,12 @@ class ErrorsTest < ActiveModel::TestCase assert_empty errors.details[:name] end + test "delete returns the deleted messages" do + errors = ActiveModel::Errors.new(Person.new) + errors.add(:name, :invalid) + assert_equal ["is invalid"], errors.delete(:name) + end + test "clear removes details" do person = Person.new person.errors.add(:name, :invalid) diff --git a/activemodel/test/cases/validations/length_validation_test.rb b/activemodel/test/cases/validations/length_validation_test.rb index 209903898e..ee901b75fb 100644 --- a/activemodel/test/cases/validations/length_validation_test.rb +++ b/activemodel/test/cases/validations/length_validation_test.rb @@ -319,8 +319,14 @@ class LengthValidationTest < ActiveModel::TestCase end def test_validates_length_of_with_block - Topic.validates_length_of :content, minimum: 5, too_short: "Your essay must be at least %{count} words.", - tokenizer: lambda {|str| str.scan(/\w+/) } + assert_deprecated do + Topic.validates_length_of( + :content, + minimum: 5, + too_short: "Your essay must be at least %{count} words.", + tokenizer: lambda {|str| str.scan(/\w+/) }, + ) + end t = Topic.new(content: "this content should be long enough") assert t.valid? @@ -332,8 +338,14 @@ class LengthValidationTest < ActiveModel::TestCase def test_validates_length_of_with_symbol - Topic.validates_length_of :content, minimum: 5, too_short: "Your essay must be at least %{count} words.", - tokenizer: :my_word_tokenizer + assert_deprecated do + Topic.validates_length_of( + :content, + minimum: 5, + too_short: "Your essay must be at least %{count} words.", + tokenizer: :my_word_tokenizer, + ) + end t = Topic.new(content: "this content should be long enough") assert t.valid? diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 0f99f907e8..d30c8e345f 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,111 @@ +* Fix missing index when using `timestamps` with the `index` option. + + The `index` option used with `timestamps` should be passed to both + `column` definitions for `created_at` and `updated_at` rather than just + the first. + + *Paul Mucur* + +* Rename `:class` to `:anonymous_class` in association options. + + Fixes #19659. + + *Andrew White* + +* Autosave existing records on a has many through association when the parent + is new. + + Fixes #19782. + + *Sean Griffin* + +* Fixed a bug where uniqueness validations would error on out of range values, + even if an validation should have prevented it from hitting the database. + + *Andrey Voronkov* + +* MySQL: `:charset` and `:collation` support for string and text columns. + + Example: + + create_table :foos do |t| + t.string :string_utf8_bin, charset: 'utf8', collation: 'utf8_bin' + t.text :text_ascii, charset: 'ascii' + end + + *Ryuta Kamizono* + +* Foreign key related methods in the migration DSL respect + `ActiveRecord::Base.pluralize_table_names = false`. + + Fixes #19643. + + *Mehmet Emin İNAÇ* + +* Reduce memory usage from loading types on PostgreSQL. + + Fixes #19578. + + *Sean Griffin* + +* Add `config.active_record.warn_on_records_fetched_greater_than` option. + + When set to an integer, a warning will be logged whenever a result set + larger than the specified size is returned by a query. + + Fixes #16463. + + *Jason Nochlin* + +* Ignore `.psqlrc` when loading database structure. + + *Jason Weathered* + +* Fix referencing wrong table aliases while joining tables of has many through + association (only when calling calculation methods). + + Fixes #19276. + + *pinglamb* + +* Correctly persist a serialized attribute that has been returned to + its default value by an in-place modification. + + Fixes #19467. + + *Matthew Draper* + +* Fix generating the schema file when using PostgreSQL `BigInt[]` data type. + Previously the `limit: 8` was not coming through, and this caused it to + become `Int[]` data type after rebuilding from the schema. + + Fixes #19420. + + *Jake Waller* + +* Reuse the `CollectionAssociation#reader` cache when the foreign key is + available prior to save. + + *Ben Woosley* + +* Add `config.active_record.dump_schemas` to fix `db:structure:dump` + when using schema_search_path and PostgreSQL extensions. + + Fixes #17157. + + *Ryan Wallace* + +* Renaming `use_transactional_fixtures` to `use_transactional_tests` for clarity. + + Fixes #18864. + + *Brandon Weiss* + +* Increase pg gem version requirement to `~> 0.18`. Earlier versions of the + pg gem are known to have problems with Ruby 2.2. + + *Matt Brictson* + * Correctly dump `serial` and `bigserial`. *Ryuta Kamizono* @@ -6,8 +114,8 @@ *James Cox* -* Dont enroll records in the transaction if they dont have commit callbacks. - That was causing a memory grow problem when creating a lot of records inside a transaction. +* Don't enroll records in the transaction if they don't have commit callbacks. + This was causing a memory leak when creating many records inside a transaction. Fixes #15549. @@ -20,11 +128,11 @@ *Sean Griffin* -* Add `SchemaMigration.create_table` support any unicode charsets for MySQL. +* Add `SchemaMigration.create_table` support for any unicode charsets with MySQL. *Ryuta Kamizono* -* PostgreSQL, no longer disables user triggers if system triggers can't be +* PostgreSQL no longer disables user triggers if system triggers can't be disabled. Disabling user triggers does not fulfill what the method promises. Rails currently requires superuser privileges for this method. @@ -38,12 +146,12 @@ *Toby Ovod-Everett*, *Yves Senn* -* PostgreSQL, print warning message if `disable_referential_integrity` fails - due to missing permissions. +* In PostgreSQL, print a warning message if `disable_referential_integrity` + fails due to missing permissions. *Andrey Nering*, *Yves Senn* -* Allow `:limit` option for MySQL bigint primary key support. +* Allow a `:limit` option for MySQL bigint primary key support. Example: @@ -66,7 +174,7 @@ *Josef Šimánek* * Fixed ActiveRecord::Relation#becomes! and changed_attributes issues for type - column. + columns. Fixes #17139. @@ -76,7 +184,7 @@ *Ryuta Kamizono* -* Allow `:precision` option for time type columns. +* Allow a `:precision` option for time type columns. *Ryuta Kamizono* @@ -93,10 +201,10 @@ recipients: commentable.recipients } end - That's what you want the bulk of the time. New comment creates a new - Notification. But there may well be off cases, like copying a commentable - and its comments, where you don't want that. So you'd have a concern - something like this: + That's what you want the bulk of the time. A new comment creates a new + Notification. There may be edge cases where you don't want that, like + when copying a commentable and its comments, in which case write a + concern with something like this: module Copyable def copy_to(destination) @@ -115,7 +223,7 @@ *Hyonjee Joo* -* Deprecated passing of `start` value to `find_in_batches` and `find_each` +* Deprecate passing of `start` value to `find_in_batches` and `find_each` in favour of `begin_at` value. *Vipul A M* @@ -124,9 +232,10 @@ *Tõnis Simo* -* Use SQL COUNT and LIMIT 1 queries for `none?` and `one?` methods if no block or limit is given, - instead of loading the entire collection to memory. - This applies to relations (e.g. `User.all`) as well as associations (e.g. `account.users`) +* Use SQL COUNT and LIMIT 1 queries for `none?` and `one?` methods + if no block or limit is given, instead of loading the entire + collection into memory. This applies to relations (e.g. `User.all`) + as well as associations (e.g. `account.users`) # Before: @@ -175,16 +284,16 @@ *Vipul A M* -* Fix rounding problem for PostgreSQL timestamp column. +* Fix a rounding problem for PostgreSQL timestamp columns. - If timestamp column have the precision, it need to format according to - the precision of timestamp column. + If a timestamp column has a precision specified, it needs to + format according to that. *Ryuta Kamizono* * Respect the database default charset for `schema_migrations` table. - The charset of `version` column in `schema_migrations` table is depend + The charset of `version` column in `schema_migrations` table depends on the database default charset and collation rather than the encoding of the connection. @@ -192,12 +301,12 @@ * Raise `ArgumentError` when passing `nil` or `false` to `Relation#merge`. - These are not valid values to merge in a relation so it should warn the users + These are not valid values to merge in a relation, so it should warn users early. *Rafael Mendonça França* -* Use `SCHEMA` instead of `DB_STRUCTURE` for specifying structure file. +* Use `SCHEMA` instead of `DB_STRUCTURE` for specifying a structure file. This makes the db:structure tasks consistent with test:load_structure. @@ -209,7 +318,7 @@ *Sean Griffin* -* Fixed several edge cases which could result in a counter cache updating +* Fix several edge cases which could result in a counter cache updating twice or not updating at all for `has_many` and `has_many :through`. Fixes #10865. @@ -242,11 +351,13 @@ *Sammy Larbi* * Change the default error message from `can't be blank` to `must exist` for - the presence validator of the `:required` option on `belongs_to`/`has_one` associations. + the presence validator of the `:required` option on `belongs_to`/`has_one` + associations. *Henrik Nygren* -* Fixed ActiveRecord::Relation#group method when argument is SQL reserved key word: +* Fixed ActiveRecord::Relation#group method when an argument is an SQL + reserved key word: Example: @@ -377,7 +488,7 @@ *Yves Senn* -* Remove deprecation when modifying a relation with cached arel. +* Remove deprecation when modifying a relation with cached Arel. This raises an `ImmutableRelation` error instead. *Yves Senn* @@ -467,13 +578,13 @@ *Florian Weingarten* -* Fixed setting of foreign_key for through associations while building of new record. +* Fix setting of foreign_key for through associations when building a new record. Fixes #12698. *Ivan Antropov* -* Improve a dump of the primary key support. If it is not a default primary key, +* Improve dumping of the primary key. If it is not a default primary key, correctly dump the type and options. Fixes #14169, #16599. @@ -491,18 +602,18 @@ *Ryuta Kamizono* -* Allow precision option for MySQL datetimes. +* Allow a precision option for MySQL datetimes. *Ryuta Kamizono* -* Fixed automatic inverse_of for models nested in module. +* Fixed automatic `inverse_of` for models nested in a module. *Andrew McCloud* * Change `ActiveRecord::Relation#update` behavior so that it can be called without passing ids of the records to be updated. - This change allows to update multiple records returned by + This change allows updating multiple records returned by `ActiveRecord::Relation` with callbacks and validations. # Before @@ -534,6 +645,13 @@ *Rafael Mendonça França* +* Fix change detection problem for PostgreSQL bytea type and + `ArgumentError: string contains null byte` exception with pg-0.18. + + Fixes #17680. + + *Lars Kanis* + * When a table has a composite primary key, the `primary_key` method for SQLite3 and PostgreSQL adapters was only returning the first field of the key. Ensures that it will return nil instead, as Active Record doesn't support @@ -543,7 +661,7 @@ *arthurnn* -* `validates_size_of` / `validates_length_of` do not count records, +* `validates_size_of` / `validates_length_of` do not count records which are `marked_for_destruction?`. Fixes #7247. @@ -585,7 +703,7 @@ *Ryuta Kamizono* -* Support for any type primary key. +* Support for any type of primary key. Fixes #14194. @@ -611,7 +729,7 @@ *Yves Senn* -* Fixes bug with 'ActiveRecord::Type::Numeric' that causes negative values to +* Fix bug with 'ActiveRecord::Type::Numeric' that caused negative values to be marked as having changed when set to the same negative value. Closes #18161. diff --git a/activerecord/activerecord.gemspec b/activerecord/activerecord.gemspec index c5cd0c89f7..bd95b57303 100644 --- a/activerecord/activerecord.gemspec +++ b/activerecord/activerecord.gemspec @@ -7,7 +7,7 @@ Gem::Specification.new do |s| s.summary = 'Object-relational mapper framework (part of Rails).' s.description = 'Databases on Rails. Build a persistent domain model by mapping database tables to Ruby classes. Strong conventions for associations, validations, aggregations, migrations, and testing come baked-in.' - s.required_ruby_version = '>= 2.2.0' + s.required_ruby_version = '>= 2.2.2' s.license = 'MIT' diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb index ef14e065ae..1844b29ccb 100644 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -43,11 +43,13 @@ module ActiveRecord autoload :Explain autoload :Inheritance autoload :Integration + autoload :LegacyYamlAdapter autoload :Migration autoload :Migrator, 'active_record/migration' autoload :ModelSchema autoload :NestedAttributes autoload :NoTouching + autoload :TouchLater autoload :Persistence autoload :QueryCache autoload :Querying diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index cecb2dbd0b..c5c2178ee2 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -113,19 +113,19 @@ module ActiveRecord # These classes will be loaded when associations are created. # So there is no need to eager load them. - autoload :Association, 'active_record/associations/association' - autoload :SingularAssociation, 'active_record/associations/singular_association' - autoload :CollectionAssociation, 'active_record/associations/collection_association' - autoload :ForeignAssociation, 'active_record/associations/foreign_association' - autoload :CollectionProxy, 'active_record/associations/collection_proxy' + autoload :Association + autoload :SingularAssociation + autoload :CollectionAssociation + autoload :ForeignAssociation + autoload :CollectionProxy - autoload :BelongsToAssociation, 'active_record/associations/belongs_to_association' - autoload :BelongsToPolymorphicAssociation, 'active_record/associations/belongs_to_polymorphic_association' - autoload :HasManyAssociation, 'active_record/associations/has_many_association' - autoload :HasManyThroughAssociation, 'active_record/associations/has_many_through_association' - autoload :HasOneAssociation, 'active_record/associations/has_one_association' - autoload :HasOneThroughAssociation, 'active_record/associations/has_one_through_association' - autoload :ThroughAssociation, 'active_record/associations/through_association' + autoload :BelongsToAssociation + autoload :BelongsToPolymorphicAssociation + autoload :HasManyAssociation + autoload :HasManyThroughAssociation + autoload :HasOneAssociation + autoload :HasOneThroughAssociation + autoload :ThroughAssociation module Builder #:nodoc: autoload :Association, 'active_record/associations/builder/association' @@ -139,10 +139,10 @@ module ActiveRecord end eager_autoload do - autoload :Preloader, 'active_record/associations/preloader' - autoload :JoinDependency, 'active_record/associations/join_dependency' - autoload :AssociationScope, 'active_record/associations/association_scope' - autoload :AliasTracker, 'active_record/associations/alias_tracker' + autoload :Preloader + autoload :JoinDependency + autoload :AssociationScope + autoload :AliasTracker end # Returns the association instance for the given name, instantiating it if it doesn't already exist @@ -1565,10 +1565,7 @@ module ActiveRecord # # class CreateDevelopersProjectsJoinTable < ActiveRecord::Migration # def change - # create_table :developers_projects, id: false do |t| - # t.integer :developer_id - # t.integer :project_id - # end + # create_join_table :developers, :projects # end # end # @@ -1718,10 +1715,8 @@ module ActiveRecord join_model = builder.through_model - # FIXME: we should move this to the internal constants. Also people - # should never directly access this constant so I'm not happy about - # setting it. const_set join_model.name, join_model + private_constant join_model.name middle_reflection = builder.middle_reflection join_model diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb index 0d8e4ba870..930f678ae8 100644 --- a/activerecord/lib/active_record/associations/association.rb +++ b/activerecord/lib/active_record/associations/association.rb @@ -8,12 +8,12 @@ module ActiveRecord # # Association # SingularAssociation - # HasOneAssociation + # HasOneAssociation + ForeignAssociation # HasOneThroughAssociation + ThroughAssociation # BelongsToAssociation # BelongsToPolymorphicAssociation # CollectionAssociation - # HasManyAssociation + # HasManyAssociation + ForeignAssociation # HasManyThroughAssociation + ThroughAssociation class Association #:nodoc: attr_reader :owner, :target, :reflection diff --git a/activerecord/lib/active_record/associations/builder/association.rb b/activerecord/lib/active_record/associations/builder/association.rb index 88406740d8..ba1b1814d1 100644 --- a/activerecord/lib/active_record/associations/builder/association.rb +++ b/activerecord/lib/active_record/associations/builder/association.rb @@ -16,7 +16,7 @@ module ActiveRecord::Associations::Builder end self.extensions = [] - VALID_OPTIONS = [:class_name, :class, :foreign_key, :validate] # :nodoc: + VALID_OPTIONS = [:class_name, :anonymous_class, :foreign_key, :validate] # :nodoc: def self.build(model, name, scope, options, &block) if model.dangerous_attribute_method?(name) diff --git a/activerecord/lib/active_record/associations/builder/belongs_to.rb b/activerecord/lib/active_record/associations/builder/belongs_to.rb index ec135d49b7..97eb007f62 100644 --- a/activerecord/lib/active_record/associations/builder/belongs_to.rb +++ b/activerecord/lib/active_record/associations/builder/belongs_to.rb @@ -60,7 +60,7 @@ module ActiveRecord::Associations::Builder klass.attr_readonly cache_column if klass && klass.respond_to?(:attr_readonly) end - def self.touch_record(o, foreign_key, name, touch) # :nodoc: + def self.touch_record(o, foreign_key, name, touch, touch_method) # :nodoc: old_foreign_id = o.changed_attributes[foreign_key] if old_foreign_id @@ -75,9 +75,9 @@ module ActiveRecord::Associations::Builder if old_record if touch != true - old_record.touch touch + old_record.send(touch_method, touch) else - old_record.touch + old_record.send(touch_method) end end end @@ -85,9 +85,9 @@ module ActiveRecord::Associations::Builder record = o.send name if record && record.persisted? if touch != true - record.touch touch + record.send(touch_method, touch) else - record.touch + record.send(touch_method) end end end @@ -98,7 +98,8 @@ module ActiveRecord::Associations::Builder touch = reflection.options[:touch] callback = lambda { |record| - BelongsTo.touch_record(record, foreign_key, n, touch) + touch_method = touching_delayed_records? ? :touch : :touch_later + BelongsTo.touch_record(record, foreign_key, n, touch, touch_method) } model.after_save callback, if: :changed? diff --git a/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb index 93dc4ae118..ffd9c9d6fc 100644 --- a/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb +++ b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb @@ -78,14 +78,14 @@ module ActiveRecord::Associations::Builder join_model.table_name_resolver = habtm join_model.class_resolver = lhs_model - join_model.add_left_association :left_side, class: lhs_model + join_model.add_left_association :left_side, anonymous_class: lhs_model join_model.add_right_association association_name, belongs_to_options(options) join_model end def middle_reflection(join_model) middle_name = [lhs_model.name.downcase.pluralize, - association_name].join('_').gsub(/::/, '_').to_sym + association_name].join('_'.freeze).gsub('::'.freeze, '_'.freeze).to_sym middle_options = middle_options join_model HasMany.create_reflection(lhs_model, diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index 0ba03338f6..6caadb4ce8 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -33,10 +33,10 @@ module ActiveRecord reload end - if owner.new_record? + if null_scope? # Cache the proxy separately before the owner has an id # or else a post-save proxy will still lack the id - @new_record_proxy ||= CollectionProxy.create(klass, self) + @null_proxy ||= CollectionProxy.create(klass, self) else @proxy ||= CollectionProxy.create(klass, self) end @@ -370,6 +370,8 @@ module ActiveRecord replace_common_records_in_memory(other_array, original_target) if other_array != original_target transaction { replace_records(other_array, original_target) } + else + other_array end end end @@ -432,8 +434,7 @@ module ActiveRecord def get_records if reflection.scope_chain.any?(&:any?) || scope.eager_loading? || - klass.current_scope || - klass.default_scopes.any? + klass.scope_attributes? return scope.to_a 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 4897ec44e9..cd79266952 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -38,12 +38,10 @@ module ActiveRecord def insert_record(record, validate = true, raise = false) ensure_not_nested - if record.new_record? - if raise - record.save!(:validate => validate) - else - return unless record.save(:validate => validate) - end + if raise + record.save!(:validate => validate) + else + return unless record.save(:validate => validate) end save_through_record(record) @@ -135,7 +133,7 @@ module ActiveRecord if scope.klass.primary_key count = scope.destroy_all.length else - scope.each(&:_run_destroy_callbacks) + scope.each { |record| record.run_callbacks :destroy } arel = scope.arel diff --git a/activerecord/lib/active_record/associations/singular_association.rb b/activerecord/lib/active_record/associations/singular_association.rb index c44242a0f0..58d0f7d65d 100644 --- a/activerecord/lib/active_record/associations/singular_association.rb +++ b/activerecord/lib/active_record/associations/singular_association.rb @@ -41,8 +41,7 @@ module ActiveRecord def get_records if reflection.scope_chain.any?(&:any?) || scope.eager_loading? || - klass.current_scope || - klass.default_scopes.any? + klass.scope_attributes? return scope.limit(1).to_a end diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index cc03e37a12..9c5b7d937d 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -120,23 +120,22 @@ module ActiveRecord #:nodoc: # All column values are automatically available through basic accessors on the Active Record # object, but sometimes you want to specialize this behavior. This can be done by overwriting # the default accessors (using the same name as the attribute) and calling - # <tt>read_attribute(attr_name)</tt> and <tt>write_attribute(attr_name, value)</tt> to actually - # change things. + # +super+ to actually change things. # # class Song < ActiveRecord::Base # # Uses an integer of seconds to hold the length of the song # # def length=(minutes) - # write_attribute(:length, minutes.to_i * 60) + # super(minutes.to_i * 60) # end # # def length - # read_attribute(:length) / 60 + # super / 60 # end # end # # You can alternatively use <tt>self[:attribute]=(value)</tt> and <tt>self[:attribute]</tt> - # instead of <tt>write_attribute(:attribute, value)</tt> and <tt>read_attribute(:attribute)</tt>. + # or <tt>write_attribute(:attribute, value)</tt> and <tt>read_attribute(:attribute)</tt>. # # == Attribute query methods # @@ -258,7 +257,7 @@ module ActiveRecord #:nodoc: # <tt>attributes=</tt> method. The +errors+ property of this exception contains an array of # AttributeAssignmentError # objects that should be inspected to determine which attributes triggered the errors. - # * RecordInvalid - raised by save! and create! when the record is invalid. + # * RecordInvalid - raised by <tt>save!</tt> and <tt>create!</tt> when the record is invalid. # * RecordNotFound - No record responded to the +find+ method. Either the row with the given ID doesn't exist # or the row didn't meet the additional restrictions. Some +find+ calls do not raise this exception to signal # nothing was found, please check its documentation for further details. @@ -309,6 +308,7 @@ module ActiveRecord #:nodoc: include Aggregations include Transactions include NoTouching + include TouchLater include Reflection include Serialization include Store diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb index f44e5af5de..2fcba8e309 100644 --- a/activerecord/lib/active_record/callbacks.rb +++ b/activerecord/lib/active_record/callbacks.rb @@ -289,25 +289,24 @@ module ActiveRecord end def destroy #:nodoc: - _run_destroy_callbacks { super } + run_callbacks(:destroy) { super } end def touch(*) #:nodoc: - _run_touch_callbacks { super } + run_callbacks(:touch) { super } end private - def create_or_update(*) #:nodoc: - _run_save_callbacks { super } + run_callbacks(:save) { super } end def _create_record #:nodoc: - _run_create_callbacks { super } + run_callbacks(:create) { super } end def _update_record(*) #:nodoc: - _run_update_callbacks { super } + run_callbacks(:update) { super } 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 d99dc9a5db..8c50f3d1a3 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -358,7 +358,7 @@ module ActiveRecord synchronize do owner = conn.owner - conn._run_checkin_callbacks do + conn.run_callbacks :checkin do conn.expire end @@ -449,7 +449,7 @@ module ActiveRecord end def checkout_and_verify(c) - c._run_checkout_callbacks do + c.run_callbacks :checkout do c.verify! end c 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 42ad285340..42c794c828 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -136,7 +136,7 @@ module ActiveRecord # # In order to get around this problem, #transaction will emulate the effect # of nested transactions, by using savepoints: - # http://dev.mysql.com/doc/refman/5.0/en/savepoint.html + # http://dev.mysql.com/doc/refman/5.6/en/savepoint.html # Savepoints are supported by MySQL and PostgreSQL. SQLite3 version >= '3.6.8' # supports savepoints. # @@ -189,7 +189,7 @@ module ActiveRecord # semantics of these different levels: # # * http://www.postgresql.org/docs/9.1/static/transaction-iso.html - # * https://dev.mysql.com/doc/refman/5.0/en/set-transaction.html + # * https://dev.mysql.com/doc/refman/5.6/en/set-transaction.html # # An <tt>ActiveRecord::TransactionIsolationError</tt> will be raised if: # diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb index d2840b9498..91c7298983 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb @@ -77,7 +77,7 @@ module ActiveRecord # Quotes a string, escaping any ' (single quote) and \ (backslash) # characters. def quote_string(s) - s.gsub(/\\/, '\&\&').gsub(/'/, "''") # ' (for ruby-mode) + s.gsub('\\'.freeze, '\&\&'.freeze).gsub("'".freeze, "''".freeze) # ' (for ruby-mode) end # Quotes the column name. Defaults to no quoting. 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 a768ee2d70..4761024ad0 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -138,7 +138,7 @@ module ActiveRecord end def foreign_table_name - name.to_s.pluralize + Base.pluralize_table_names ? name.to_s.pluralize : name end end @@ -359,6 +359,7 @@ module ActiveRecord def column(name, type, options = {}) name = name.to_s type = type.to_sym + options = options.dup if @columns_hash[name] && @columns_hash[name].primary_key? raise ArgumentError, "you can't redefine the primary key column '#{name}'. To define a custom primary key, pass { id: false } to create_table." 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 d42f9a894b..879a47f021 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -120,6 +120,8 @@ module ActiveRecord # [<tt>:id</tt>] # Whether to automatically add a primary key column. Defaults to true. # Join tables for +has_and_belongs_to_many+ should set it to false. + # + # A Symbol can be used to specify the type of the generated primary key column. # [<tt>:primary_key</tt>] # The name of the primary key, if one is to be added automatically. # Defaults to +id+. If <tt>:id</tt> is false this option is ignored. @@ -163,6 +165,19 @@ module ActiveRecord # name varchar(80) # ) # + # ====== Change the primary key column type + # + # create_table(:tags, id: :string) do |t| + # t.column :label, :string + # end + # + # generates: + # + # CREATE TABLE tags ( + # id varchar PRIMARY KEY, + # label varchar + # ) + # # ====== Do not add a primary key column # # create_table(:categories_suppliers, id: false) do |t| @@ -377,6 +392,9 @@ module ActiveRecord # [<tt>:force</tt>] # Set to +:cascade+ to drop dependent objects as well. # Defaults to false. + # [<tt>:if_exists</tt>] + # Set to +true+ to only drop the table if it exists. + # Defaults to false. # # Although this command ignores most +options+ and the block if one is given, # it can be helpful to provide these in a migration's +change+ method so it can be reverted. @@ -387,6 +405,10 @@ module ActiveRecord # Adds a new column to the named table. # See TableDefinition#column for details of the options you can use. + # + # Note: Not all options will be available, generally this command should + # ignore most of them. In favor of doing a low-level call to simply + # create a column. def add_column(table_name, column_name, type, options = {}) at = create_alter_table table_name at.add_column(column_name, type, options) @@ -530,6 +552,8 @@ module ActiveRecord # # CREATE UNIQUE INDEX index_accounts_on_branch_id_and_party_id ON accounts(branch_id, party_id) WHERE active # + # Note: Partial indexes are only supported for PostgreSQL and SQLite 3.8.0+. + # # ====== Creating an index with a specific method # # add_index(:developers, :name, using: 'btree') @@ -652,7 +676,7 @@ module ActiveRecord alias :add_belongs_to :add_reference # Removes the reference(s). Also removes a +type+ column if one exists. - # <tt>remove_reference</tt>, <tt>remove_references</tt> and <tt>remove_belongs_to</tt> are acceptable. + # <tt>remove_reference</tt> and <tt>remove_belongs_to</tt> are acceptable. # # ====== Remove the reference # @@ -667,7 +691,10 @@ module ActiveRecord # remove_reference(:products, :user, index: true, foreign_key: true) # def remove_reference(table_name, ref_name, options = {}) - remove_foreign_key table_name, ref_name.to_s.pluralize if options[:foreign_key] + if options[:foreign_key] + reference_name = Base.pluralize_table_names ? ref_name.to_s.pluralize : ref_name + remove_foreign_key(table_name, reference_name) + end remove_column(table_name, "#{ref_name}_id") remove_column(table_name, "#{ref_name}_type") if options[:polymorphic] @@ -684,8 +711,8 @@ module ActiveRecord # +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. + # +identifier+ is a 10 character long string which is deterministically generated from the + # +from_table+ and +column+. A custom name can be specified with the <tt>:name</tt> option. # # ====== Creating a simple foreign key # diff --git a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb index c74f4e8fa5..295a7bed87 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb @@ -44,11 +44,12 @@ module ActiveRecord attr_reader :connection, :state, :records, :savepoint_name attr_writer :joinable - def initialize(connection, options) + def initialize(connection, options, run_commit_callbacks: false) @connection = connection @state = TransactionState.new @records = [] @joinable = options.fetch(:joinable, true) + @run_commit_callbacks = run_commit_callbacks end def add_record(record) @@ -75,18 +76,21 @@ module ActiveRecord end def before_commit_records - records.uniq.each(&:before_committed!) + records.uniq.each(&:before_committed!) if @run_commit_callbacks end def commit_records ite = records.uniq while record = ite.shift - record.committed! + if @run_commit_callbacks + record.committed! + else + # if not running callbacks, only adds the record to the parent transaction + record.add_to_transaction + end end ensure - ite.each do |i| - i.committed!(should_run_callbacks: false) - end + ite.each { |i| i.committed!(should_run_callbacks: false) } end def full_rollback?; true; end @@ -97,8 +101,8 @@ module ActiveRecord class SavepointTransaction < Transaction - def initialize(connection, savepoint_name, options) - super(connection, options) + def initialize(connection, savepoint_name, options, *args) + super(connection, options, *args) if options[:isolation] raise ActiveRecord::TransactionIsolationError, "cannot set transaction isolation in a nested transaction" end @@ -120,7 +124,7 @@ module ActiveRecord class RealTransaction < Transaction - def initialize(connection, options) + def initialize(connection, options, *args) super if options[:isolation] connection.begin_isolated_db_transaction(options[:isolation]) @@ -147,11 +151,13 @@ module ActiveRecord end def begin_transaction(options = {}) + run_commit_callbacks = !current_transaction.joinable? transaction = if @stack.empty? - RealTransaction.new(@connection, options) + RealTransaction.new(@connection, options, run_commit_callbacks: run_commit_callbacks) else - SavepointTransaction.new(@connection, "active_record_#{@stack.size}", options) + SavepointTransaction.new(@connection, "active_record_#{@stack.size}", options, + run_commit_callbacks: run_commit_callbacks) end @stack.push(transaction) @@ -159,18 +165,11 @@ module ActiveRecord end def commit_transaction - inner_transaction = @stack.pop - - if current_transaction.joinable? - inner_transaction.commit - inner_transaction.records.each do |r| - r.add_to_transaction - end - else - inner_transaction.before_commit_records - inner_transaction.commit - inner_transaction.commit_records - end + transaction = @stack.last + transaction.before_commit_records + @stack.pop + transaction.commit + transaction.commit_records end def rollback_transaction(transaction = nil) 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 9625fcf9b8..76aee452ca 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -13,6 +13,10 @@ module ActiveRecord end end + class ColumnDefinition < ActiveRecord::ConnectionAdapters::ColumnDefinition + attr_accessor :charset, :collation + end + class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition include ColumnMethods @@ -23,8 +27,16 @@ module ActiveRecord column.type = :integer column.auto_increment = true end + column.charset = options[:charset] + column.collation = options[:collation] column end + + private + + def create_column_definition(name, type) + ColumnDefinition.new(name, type) + end end class Table < ActiveRecord::ConnectionAdapters::Table @@ -60,6 +72,23 @@ module ActiveRecord add_column_position!(change_column_sql, column_options(o.column)) end + def column_options(o) + column_options = super + column_options[:charset] = o.charset + column_options[:collation] = o.collation + column_options + end + + def add_column_options!(sql, options) + if options[:charset] + sql << " CHARACTER SET #{options[:charset]}" + end + if options[:collation] + sql << " COLLATE #{options[:collation]}" + end + super + end + def add_column_position!(sql, options) if options[:first] sql << " FIRST" @@ -99,9 +128,18 @@ module ActiveRecord spec = super spec.delete(:precision) if /time/ === column.sql_type && column.precision == 0 spec.delete(:limit) if :boolean === column.type + if column.collation && table_name = column.instance_variable_get(:@table_name) + @collation_cache ||= {} + @collation_cache[table_name] ||= select_one("SHOW TABLE STATUS LIKE '#{table_name}'")["Collation"] + spec[:collation] = column.collation.inspect if column.collation != @collation_cache[table_name] + end spec end + def migration_keys + super + [:collation] + end + class Column < ConnectionAdapters::Column # :nodoc: delegate :strict, :collation, :extra, to: :sql_type_metadata, allow_nil: true @@ -562,6 +600,21 @@ module ActiveRecord rename_table_indexes(table_name, new_name) end + # Drops a table from the database. + # + # [<tt>:force</tt>] + # Set to +:cascade+ to drop dependent objects as well. + # Defaults to false. + # [<tt>:if_exists</tt>] + # Set to +true+ to only drop the table if it exists. + # Defaults to false. + # [<tt>:temporary</tt>] + # Set to +true+ to drop temporary table. + # Defaults to false. + # + # Although this command ignores most +options+ and the block if one is given, + # it can be helpful to provide these in a migration's +change+ method so it can be reverted. + # In that case, +options+ and the block will be used by create_table. def drop_table(table_name, options = {}) execute "DROP#{' TEMPORARY' if options[:temporary]} TABLE#{' IF EXISTS' if options[:if_exists]} #{quote_table_name(table_name)}#{' CASCADE' if options[:force] == :cascade}" end @@ -785,7 +838,9 @@ module ActiveRecord subselect = Arel::SelectManager.new(select.engine) subselect.project Arel.sql(key.name) - subselect.from subsubselect.as('__active_record_temp') + # Materialized subquery by adding distinct + # to work with MySQL 5.7.6 which sets optimizer_switch='derived_merge=on' + subselect.from subsubselect.distinct.as('__active_record_temp') end def add_index_length(option_strings, column_names, options = {}) @@ -903,8 +958,7 @@ module ActiveRecord def configure_connection variables = @config.fetch(:variables, {}).stringify_keys - # By default, MySQL 'where id is null' selects the last inserted id. - # Turn this off. http://dev.rubyonrails.org/ticket/6778 + # By default, MySQL 'where id is null' selects the last inserted id; Turn this off. variables['sql_auto_is_null'] = 0 # Increase timeout so the server doesn't disconnect us. @@ -913,14 +967,14 @@ module ActiveRecord variables['wait_timeout'] = self.class.type_cast_config_to_integer(wait_timeout) # 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 + # http://dev.mysql.com/doc/refman/5.6/en/sql-mode.html#sqlmode_strict_all_tables # If the user has provided another value for sql_mode, don't replace it. unless variables.has_key?('sql_mode') variables['sql_mode'] = strict_mode? ? 'STRICT_ALL_TABLES' : '' end # NAMES does not have an equals sign, see - # http://dev.mysql.com/doc/refman/5.0/en/set-statement.html#id944430 + # http://dev.mysql.com/doc/refman/5.6/en/set-statement.html#id944430 # (trailing comma because variable_assignments will always have content) if @config[:encoding] encoding = "NAMES #{@config[:encoding]}" diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb index a67127bd71..f4dda5154e 100644 --- a/activerecord/lib/active_record/connection_adapters/column.rb +++ b/activerecord/lib/active_record/connection_adapters/column.rb @@ -28,6 +28,7 @@ module ActiveRecord @null = null @default = default @default_function = default_function + @table_name = nil end def has_default? diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb index 64985ee933..45b935f1d6 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb @@ -56,9 +56,9 @@ module ActiveRecord # * <tt>:password</tt> - Defaults to nothing. # * <tt>:database</tt> - The name of the database. No default, must be provided. # * <tt>:encoding</tt> - (Optional) Sets the client encoding by executing "SET NAMES <encoding>" after connection. - # * <tt>:reconnect</tt> - Defaults to false (See MySQL documentation: http://dev.mysql.com/doc/refman/5.0/en/auto-reconnect.html). - # * <tt>:strict</tt> - Defaults to true. Enable STRICT_ALL_TABLES. (See MySQL documentation: http://dev.mysql.com/doc/refman/5.0/en/server-sql-mode.html) - # * <tt>:variables</tt> - (Optional) A hash session variables to send as <tt>SET @@SESSION.key = value</tt> on each database connection. Use the value +:default+ to set a variable to its DEFAULT value. (See MySQL documentation: http://dev.mysql.com/doc/refman/5.0/en/set-statement.html). + # * <tt>:reconnect</tt> - Defaults to false (See MySQL documentation: http://dev.mysql.com/doc/refman/5.6/en/auto-reconnect.html). + # * <tt>:strict</tt> - Defaults to true. Enable STRICT_ALL_TABLES. (See MySQL documentation: http://dev.mysql.com/doc/refman/5.6/en/sql-mode.html) + # * <tt>:variables</tt> - (Optional) A hash session variables to send as <tt>SET @@SESSION.key = value</tt> on each database connection. Use the value +:default+ to set a variable to its DEFAULT value. (See MySQL documentation: http://dev.mysql.com/doc/refman/5.6/en/set-statement.html). # * <tt>:sslca</tt> - Necessary to use MySQL with an SSL connection. # * <tt>:sslkey</tt> - Necessary to use MySQL with an SSL connection. # * <tt>:sslcert</tt> - Necessary to use MySQL with an SSL connection. diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/array_parser.rb b/activerecord/lib/active_record/connection_adapters/postgresql/array_parser.rb deleted file mode 100644 index 1b74c039ce..0000000000 --- a/activerecord/lib/active_record/connection_adapters/postgresql/array_parser.rb +++ /dev/null @@ -1,93 +0,0 @@ -module ActiveRecord - module ConnectionAdapters - module PostgreSQL - module ArrayParser # :nodoc: - - DOUBLE_QUOTE = '"' - BACKSLASH = "\\" - COMMA = ',' - BRACKET_OPEN = '{' - BRACKET_CLOSE = '}' - - def parse_pg_array(string) # :nodoc: - local_index = 0 - array = [] - while(local_index < string.length) - case string[local_index] - when BRACKET_OPEN - local_index,array = parse_array_contents(array, string, local_index + 1) - when BRACKET_CLOSE - return array - end - local_index += 1 - end - - array - end - - private - - def parse_array_contents(array, string, index) - is_escaping = false - is_quoted = false - was_quoted = false - current_item = '' - - local_index = index - while local_index - token = string[local_index] - if is_escaping - current_item << token - is_escaping = false - else - if is_quoted - case token - when DOUBLE_QUOTE - is_quoted = false - was_quoted = true - when BACKSLASH - is_escaping = true - else - current_item << token - end - else - case token - when BACKSLASH - is_escaping = true - when COMMA - add_item_to_array(array, current_item, was_quoted) - current_item = '' - was_quoted = false - when DOUBLE_QUOTE - is_quoted = true - when BRACKET_OPEN - internal_items = [] - local_index,internal_items = parse_array_contents(internal_items, string, local_index + 1) - array.push(internal_items) - when BRACKET_CLOSE - add_item_to_array(array, current_item, was_quoted) - return local_index,array - else - current_item << token - end - end - end - - local_index += 1 - end - return local_index,array - end - - def add_item_to_array(array, current_item, quoted) - return if !quoted && current_item.length == 0 - - if !quoted && current_item == 'NULL' - array.push nil - else - array.push current_item - end - end - end - end - end -end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb index fb4e0de2a8..3de794f797 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb @@ -5,29 +5,20 @@ module ActiveRecord class Array < Type::Value # :nodoc: include Type::Helpers::Mutable - # Loads pg_array_parser if available. String parsing can be - # performed quicker by a native extension, which will not create - # a large amount of Ruby objects that will need to be garbage - # collected. pg_array_parser has a C and Java extension - begin - require 'pg_array_parser' - include PgArrayParser - rescue LoadError - require 'active_record/connection_adapters/postgresql/array_parser' - include PostgreSQL::ArrayParser - end - attr_reader :subtype, :delimiter - delegate :type, :user_input_in_time_zone, to: :subtype + delegate :type, :user_input_in_time_zone, :limit, to: :subtype def initialize(subtype, delimiter = ',') @subtype = subtype @delimiter = delimiter + + @pg_encoder = PG::TextEncoder::Array.new name: "#{type}[]", delimiter: delimiter + @pg_decoder = PG::TextDecoder::Array.new name: "#{type}[]", delimiter: delimiter end def deserialize(value) if value.is_a?(::String) - type_cast_array(parse_pg_array(value), :deserialize) + type_cast_array(@pg_decoder.decode(value), :deserialize) else super end @@ -35,14 +26,14 @@ module ActiveRecord def cast(value) if value.is_a?(::String) - value = parse_pg_array(value) + value = @pg_decoder.decode(value) end type_cast_array(value, :cast) end def serialize(value) if value.is_a?(::Array) - cast_value_for_database(value) + @pg_encoder.encode(type_cast_array(value, :serialize)) else super end @@ -63,41 +54,6 @@ module ActiveRecord @subtype.public_send(method, value) end end - - def cast_value_for_database(value) - if value.is_a?(::Array) - casted_values = value.map { |item| cast_value_for_database(item) } - "{#{casted_values.join(delimiter)}}" - else - quote_and_escape(subtype.serialize(value)) - end - end - - ARRAY_ESCAPE = "\\" * 2 * 2 # escape the backslash twice for PG arrays - - def quote_and_escape(value) - case value - when ::String - if string_requires_quoting?(value) - value = value.gsub(/\\/, ARRAY_ESCAPE) - value.gsub!(/"/,"\\\"") - %("#{value}") - else - value - end - when nil then "NULL" - else value - end - end - - # See http://www.postgresql.org/docs/9.2/static/arrays.html#ARRAYS-IO - # for a list of all cases in which strings will be quoted. - def string_requires_quoting?(string) - string.empty? || - string == "NULL" || - string =~ /[\{\}"\\\s]/ || - string.include?(delimiter) - end end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/enum.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/enum.rb index b3b610a5f6..91d339f32c 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/enum.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/enum.rb @@ -7,7 +7,9 @@ module ActiveRecord :enum end - def cast(value) + private + + def cast_value(value) value.to_s end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb index 9b3de41fab..191c828e60 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb @@ -15,11 +15,11 @@ module ActiveRecord def run(records) nodes = records.reject { |row| @store.key? row['oid'].to_i } mapped, nodes = nodes.partition { |row| @store.key? row['typname'] } - ranges, nodes = nodes.partition { |row| row['typtype'] == 'r' } - enums, nodes = nodes.partition { |row| row['typtype'] == 'e' } - domains, nodes = nodes.partition { |row| row['typtype'] == 'd' } - arrays, nodes = nodes.partition { |row| row['typinput'] == 'array_in' } - composites, nodes = nodes.partition { |row| row['typelem'] != '0' } + ranges, nodes = nodes.partition { |row| row['typtype'] == 'r'.freeze } + enums, nodes = nodes.partition { |row| row['typtype'] == 'e'.freeze } + domains, nodes = nodes.partition { |row| row['typtype'] == 'd'.freeze } + arrays, nodes = nodes.partition { |row| row['typinput'] == 'array_in'.freeze } + composites, nodes = nodes.partition { |row| row['typelem'].to_i != 0 } mapped.each { |row| register_mapped_type(row) } enums.each { |row| register_enum_type(row) } @@ -29,6 +29,18 @@ module ActiveRecord composites.each { |row| register_composite_type(row) } end + def query_conditions_for_initial_load(type_map) + known_type_names = type_map.keys.map { |n| "'#{n}'" } + known_type_types = %w('r' 'e' 'd') + <<-SQL % [known_type_names.join(", "), known_type_types.join(", ")] + WHERE + t.typname IN (%s) + OR t.typtype IN (%s) + OR t.typinput::varchar = 'array_in' + OR t.typelem != 0 + SQL + end + private def register_mapped_type(row) alias_type row['oid'], row['typname'] diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb b/activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb index c1835380f9..44a7338bf5 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb @@ -14,7 +14,7 @@ module ActiveRecord transaction(requires_new: true) do execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} DISABLE TRIGGER ALL" }.join(";")) end - rescue => e + rescue ActiveRecord::ActiveRecordError => e original_exception = e end @@ -34,10 +34,10 @@ Rails needs superuser privileges to disable referential integrity. end begin - transaction(require_new: true) do + transaction(requires_new: true) do execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} ENABLE TRIGGER ALL" }.join(";")) end - rescue + rescue ActiveRecord::ActiveRecordError end else yield 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 eeb141dd1e..168180cfd3 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -87,7 +87,7 @@ module ActiveRecord SQL end - def drop_table(table_name, options = {}) + def drop_table(table_name, options = {}) # :nodoc: execute "DROP TABLE#{' IF EXISTS' if options[:if_exists]} #{quote_table_name(table_name)}#{' CASCADE' if options[:force] == :cascade}" end @@ -129,8 +129,8 @@ module ActiveRecord result.map do |row| index_name = row[0] - unique = row[1] == 't' - indkey = row[2].split(" ") + unique = row[1] + indkey = row[2].split(" ").map(&:to_i) inddef = row[3] oid = row[4] @@ -164,7 +164,7 @@ module ActiveRecord type_metadata = fetch_type_metadata(column_name, type, oid, fmod) default_value = extract_value_from_default(default) default_function = extract_default_function(default_value, default) - new_column(column_name, default_value, type_metadata, notnull == 'f', default_function) + new_column(column_name, default_value, type_metadata, !notnull, default_function) end end @@ -422,7 +422,7 @@ module ActiveRecord end # Changes the default value of a table column. - def change_column_default(table_name, column_name, default) + def change_column_default(table_name, column_name, default) # :nodoc: clear_cache! column = column_for(table_name, column_name) return unless column diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 92f470ae70..332ac9d88c 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -1,3 +1,7 @@ +# Make sure we're using pg high enough for type casts and Ruby 2.2+ compatibility +gem 'pg', '~> 0.18' +require 'pg' + require "active_record/connection_adapters/abstract_adapter" require "active_record/connection_adapters/postgresql/column" require "active_record/connection_adapters/postgresql/database_statements" @@ -12,10 +16,6 @@ require "active_record/connection_adapters/statement_pool" require 'arel/visitors/bind_visitor' -# Make sure we're using pg high enough for PGResult#values -gem 'pg', '~> 0.15' -require 'pg' - require 'ipaddr' module ActiveRecord @@ -278,8 +278,7 @@ module ActiveRecord @table_alias_length = nil connect - add_pg_decoders - + add_pg_encoders @statements = StatementPool.new @connection, self.class.type_cast_config_to_integer(config.fetch(:statement_limit) { 1000 }) @@ -287,6 +286,8 @@ module ActiveRecord raise "Your version of PostgreSQL (#{postgresql_version}) is too old, please upgrade!" end + add_pg_decoders + @type_map = Type::HashLookupTypeMap.new initialize_type_map(type_map) @local_tz = execute('SHOW TIME ZONE', 'SCHEMA').first["TimeZone"] @@ -567,9 +568,9 @@ module ActiveRecord case default # Quoted types when /\A[\(B]?'(.*)'::/m - $1.gsub(/''/, "'") + $1.gsub("''".freeze, "'".freeze) # Boolean types - when 'true', 'false' + when 'true'.freeze, 'false'.freeze default # Numeric types when /\A\(?(-?\d+(\.\d*)?)\)?(::bigint)?\z/ @@ -593,6 +594,8 @@ module ActiveRecord end def load_additional_types(type_map, oids = nil) # :nodoc: + initializer = OID::TypeMapInitializer.new(type_map) + if supports_ranges? query = <<-SQL SELECT t.oid, t.typname, t.typelem, t.typdelim, t.typinput, r.rngsubtype, t.typtype, t.typbasetype @@ -608,11 +611,13 @@ module ActiveRecord if oids query += "WHERE t.oid::integer IN (%s)" % oids.join(", ") + else + query += initializer.query_conditions_for_initial_load(type_map) end - initializer = OID::TypeMapInitializer.new(type_map) - records = execute(query, 'SCHEMA') - initializer.run(records) + execute_and_clear(query, 'SCHEMA', []) do |records| + initializer.run(records) + end end FEATURE_NOT_SUPPORTED = "0A000" #:nodoc: @@ -798,11 +803,20 @@ module ActiveRecord ) end_sql execute_and_clear(sql, "SCHEMA", []) do |result| - result.getvalue(0, 0) == 't' + result.getvalue(0, 0) end end end + def add_pg_encoders + map = PG::TypeMapByClass.new + map[Integer] = PG::TextEncoder::Integer.new + map[TrueClass] = PG::TextEncoder::Boolean.new + map[FalseClass] = PG::TextEncoder::Boolean.new + map[Float] = PG::TextEncoder::Float.new + @connection.type_map_for_queries = map + end + def add_pg_decoders coders_by_name = { 'int2' => PG::TextDecoder::Integer, @@ -813,13 +827,15 @@ module ActiveRecord 'float8' => PG::TextDecoder::Float, 'bool' => PG::TextDecoder::Boolean, } - query = <<-SQL - SELECT t.oid, t.typname, t.typelem, t.typdelim, t.typinput, t.typtype, t.typbasetype + known_coder_types = coders_by_name.keys.map { |n| quote(n) } + query = <<-SQL % known_coder_types.join(", ") + SELECT t.oid, t.typname FROM pg_type as t + WHERE t.typname IN (%s) SQL coders = execute_and_clear(query, "SCHEMA", []) do |result| result - .map { |row| construct_coder(row, coders_by_name['typname']) } + .map { |row| construct_coder(row, coders_by_name[row['typname']]) } .compact end @@ -830,7 +846,7 @@ module ActiveRecord def construct_coder(row, coder_class) return unless coder_class - coder_class.new(oid: row['oid'], name: row['typname']) + coder_class.new(oid: row['oid'].to_i, name: row['typname']) end ActiveRecord::Type.add_modifier({ array: true }, OID::Array, adapter: :postgresql) diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index 97ce4642aa..1ad910c4bc 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -85,6 +85,24 @@ module ActiveRecord mattr_accessor :dump_schema_after_migration, instance_writer: false self.dump_schema_after_migration = true + ## + # :singleton-method: + # Specifies which database schemas to dump when calling db:structure:dump. + # If :schema_search_path (the default), it will dumps any schemas listed in schema_search_path. + # Use :all to always dumps all schemas regardless of the schema_search_path. + # A string of comma separated schemas can also be used to pass a custom list of schemas. + mattr_accessor :dump_schemas, instance_writer: false + self.dump_schemas = :schema_search_path + + ## + # :singleton-method: + # Specify a threshold for the size of query result sets. If the number of + # records in the set exceeds the threshold, a warning is logged. This can + # be used to identify queries which load thousands of records and + # potentially cause memory bloat. + mattr_accessor :warn_on_records_fetched_greater_than, instance_writer: false + self.warn_on_records_fetched_greater_than = nil + mattr_accessor :maintain_test_schema, instance_accessor: false mattr_accessor :belongs_to_required_by_default, instance_accessor: false @@ -123,8 +141,7 @@ module ActiveRecord return super unless ids.length == 1 return super if block_given? || primary_key.nil? || - default_scopes.any? || - current_scope || + scope_attributes? || columns_hash.include?(inheritance_column) || ids.first.kind_of?(Array) @@ -152,8 +169,7 @@ module ActiveRecord end def find_by(*args) # :nodoc: - return super if current_scope || !(Hash === args.first) || reflect_on_all_aggregations.any? - return super if default_scopes.any? + return super if scope_attributes? || !(Hash === args.first) || reflect_on_all_aggregations.any? hash = args.first @@ -286,20 +302,25 @@ module ActiveRecord assign_attributes(attributes) if attributes yield self if block_given? - _run_initialize_callbacks + run_callbacks :initialize end - # Initialize an empty model object from +coder+. +coder+ must contain - # the attributes necessary for initializing an empty model object. For - # example: + # Initialize an empty model object from +coder+. +coder+ should be + # the result of previously encoding an Active Record model, using + # `encode_with` # # class Post < ActiveRecord::Base # end # + # old_post = Post.new(title: "hello world") + # coder = {} + # old_post.encode_with(coder) + # # post = Post.allocate - # post.init_with('attributes' => { 'title' => 'hello world' }) + # post.init_with(coder) # post.title # => 'hello world' def init_with(coder) + coder = LegacyYamlAdapter.convert(self.class, coder) @attributes = coder['attributes'] init_internals @@ -308,8 +329,8 @@ module ActiveRecord self.class.define_attribute_methods - _run_find_callbacks - _run_initialize_callbacks + run_callbacks :find + run_callbacks :initialize self end @@ -345,7 +366,7 @@ module ActiveRecord @attributes = @attributes.dup @attributes.reset(self.class.primary_key) - _run_initialize_callbacks + run_callbacks(:initialize) @new_record = true @destroyed = false @@ -370,6 +391,7 @@ module ActiveRecord coder['raw_attributes'] = attributes_before_type_cast coder['attributes'] = @attributes coder['new_record'] = new_record? + coder['active_record_yaml_version'] = 1 end # Returns true if +comparison_object+ is the same exact object, or +comparison_object+ diff --git a/activerecord/lib/active_record/enum.rb b/activerecord/lib/active_record/enum.rb index ea88983917..2b99899e42 100644 --- a/activerecord/lib/active_record/enum.rb +++ b/activerecord/lib/active_record/enum.rb @@ -32,7 +32,7 @@ module ActiveRecord # Conversation.active # Conversation.archived # - # Of course, you can also query them directly if the scopes doesn't fit your + # Of course, you can also query them directly if the scopes don't fit your # needs: # # Conversation.where(status: [:active, :archived]) diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index 18775caad2..2c1771dd6c 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -139,13 +139,13 @@ module ActiveRecord # name: kitten.png # sha: <%= file_sha 'files/kitten.png' %> # - # = Transactional Fixtures + # = Transactional Tests # # Test cases can use begin+rollback to isolate their changes to the database instead of having to # delete+insert for every test case. # # class FooTest < ActiveSupport::TestCase - # self.use_transactional_fixtures = true + # self.use_transactional_tests = true # # test "godzilla" do # assert !Foo.all.empty? @@ -159,14 +159,14 @@ module ActiveRecord # end # # If you preload your test database with all fixture data (probably in the rake task) and use - # transactional fixtures, then you may omit all fixtures declarations in your test cases since + # transactional tests, then you may omit all fixtures declarations in your test cases since # all the data's already there and every case rolls back its changes. # # In order to use instantiated fixtures with preloaded data, set +self.pre_loaded_fixtures+ to # true. This will provide access to fixture data for every table that has been loaded through # fixtures (depending on the value of +use_instantiated_fixtures+). # - # When *not* to use transactional fixtures: + # When *not* to use transactional tests: # # 1. You're testing whether a transaction works correctly. Nested transactions don't commit until # all parent transactions commit, particularly, the fixtures transaction which is begun in setup @@ -835,13 +835,15 @@ module ActiveRecord class_attribute :fixture_path, :instance_writer => false class_attribute :fixture_table_names class_attribute :fixture_class_names + class_attribute :use_transactional_tests class_attribute :use_transactional_fixtures class_attribute :use_instantiated_fixtures # true, false, or :no_instances class_attribute :pre_loaded_fixtures class_attribute :config + singleton_class.deprecate 'use_transactional_fixtures=' => 'use use_transactional_tests= instead' + self.fixture_table_names = [] - self.use_transactional_fixtures = true self.use_instantiated_fixtures = false self.pre_loaded_fixtures = false self.config = ActiveRecord::Base @@ -849,6 +851,16 @@ module ActiveRecord self.fixture_class_names = Hash.new do |h, fixture_set_name| h[fixture_set_name] = ActiveRecord::FixtureSet.default_fixture_model_name(fixture_set_name, self.config) end + + silence_warnings do + define_singleton_method :use_transactional_tests do + if use_transactional_fixtures.nil? + true + else + use_transactional_fixtures + end + end + end end module ClassMethods @@ -919,13 +931,13 @@ module ActiveRecord end def run_in_transaction? - use_transactional_fixtures && + use_transactional_tests && !self.class.uses_transaction?(method_name) end def setup_fixtures(config = ActiveRecord::Base) - if pre_loaded_fixtures && !use_transactional_fixtures - raise RuntimeError, 'pre_loaded_fixtures requires use_transactional_fixtures' + if pre_loaded_fixtures && !use_transactional_tests + raise RuntimeError, 'pre_loaded_fixtures requires use_transactional_tests' end @fixture_cache = {} diff --git a/activerecord/lib/active_record/legacy_yaml_adapter.rb b/activerecord/lib/active_record/legacy_yaml_adapter.rb new file mode 100644 index 0000000000..89dee58423 --- /dev/null +++ b/activerecord/lib/active_record/legacy_yaml_adapter.rb @@ -0,0 +1,46 @@ +module ActiveRecord + module LegacyYamlAdapter + def self.convert(klass, coder) + return coder unless coder.is_a?(Psych::Coder) + + case coder["active_record_yaml_version"] + when 1 then coder + else + if coder["attributes"].is_a?(AttributeSet) + Rails420.convert(klass, coder) + else + Rails41.convert(klass, coder) + end + end + end + + module Rails420 + def self.convert(klass, coder) + attribute_set = coder["attributes"] + + klass.attribute_names.each do |attr_name| + attribute = attribute_set[attr_name] + if attribute.type.is_a?(Delegator) + type_from_klass = klass.type_for_attribute(attr_name) + attribute_set[attr_name] = attribute.with_type(type_from_klass) + end + end + + coder + end + end + + module Rails41 + def self.convert(klass, coder) + attributes = klass.attributes_builder + .build_from_database(coder["attributes"]) + new_record = coder["attributes"][klass.primary_key].blank? + + { + "attributes" => attributes, + "new_record" => new_record, + } + end + end + end +end diff --git a/activerecord/lib/active_record/locking/pessimistic.rb b/activerecord/lib/active_record/locking/pessimistic.rb index ff7102d35b..3d95c54ef3 100644 --- a/activerecord/lib/active_record/locking/pessimistic.rb +++ b/activerecord/lib/active_record/locking/pessimistic.rb @@ -51,7 +51,7 @@ module ActiveRecord # end # # Database-specific information on row locking: - # MySQL: http://dev.mysql.com/doc/refman/5.1/en/innodb-locking-reads.html + # MySQL: http://dev.mysql.com/doc/refman/5.6/en/innodb-locking-reads.html # PostgreSQL: http://www.postgresql.org/docs/current/interactive/sql-select.html#SQL-FOR-UPDATE-SHARE module Pessimistic # Obtain a row lock on this record. Reloads the record to obtain the requested diff --git a/activerecord/lib/active_record/log_subscriber.rb b/activerecord/lib/active_record/log_subscriber.rb index 6b26d7be78..af816a278e 100644 --- a/activerecord/lib/active_record/log_subscriber.rb +++ b/activerecord/lib/active_record/log_subscriber.rb @@ -31,9 +31,10 @@ module ActiveRecord end def sql(event) - self.class.runtime += event.duration return unless logger.debug? + self.class.runtime += event.duration + payload = event.payload return if IGNORE_PAYLOAD_NAMES.include?(payload[:name]) diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb index 75adcccce6..3674f672cb 100644 --- a/activerecord/lib/active_record/model_schema.rb +++ b/activerecord/lib/active_record/model_schema.rb @@ -328,12 +328,8 @@ module ActiveRecord @default_attributes = nil @inheritance_column = nil unless defined?(@explicit_inheritance_column) && @explicit_inheritance_column @attributes_builder = nil - @column_names = nil - @attribute_types = nil @columns = nil @columns_hash = nil - @content_columns = nil - @default_attributes = nil @attribute_names = nil end diff --git a/activerecord/lib/active_record/null_relation.rb b/activerecord/lib/active_record/null_relation.rb index fa6eaa7e64..74894d0c37 100644 --- a/activerecord/lib/active_record/null_relation.rb +++ b/activerecord/lib/active_record/null_relation.rb @@ -14,7 +14,7 @@ module ActiveRecord 0 end - def update_all(_updates, _conditions = nil, _options = {}) + def update_all(_updates) 0 end @@ -80,7 +80,7 @@ module ActiveRecord end end - def exists?(_id = false) + def exists?(_conditions = :none) false end diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index a6176dffdb..ae1c326d95 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -462,9 +462,10 @@ module ActiveRecord # ball = Ball.new # ball.touch(:updated_at) # => raises ActiveRecordError # - def touch(*names, time: current_time_from_proper_timezone) + def touch(*names, time: nil) raise ActiveRecordError, "cannot touch on a new record object" unless persisted? + time ||= current_time_from_proper_timezone attributes = timestamp_attributes_for_update_in_model attributes.concat(names) @@ -476,11 +477,23 @@ module ActiveRecord changes[column] = write_attribute(column, time) end - changes[self.class.locking_column] = increment_lock if locking_enabled? - clear_attribute_changes(changes.keys) primary_key = self.class.primary_key - self.class.unscoped.where(primary_key => self[primary_key]).update_all(changes) == 1 + scope = self.class.unscoped.where(primary_key => _read_attribute(primary_key)) + + if locking_enabled? + locking_column = self.class.locking_column + scope = scope.where(locking_column => _read_attribute(locking_column)) + changes[locking_column] = increment_lock + end + + result = scope.update_all(changes) == 1 + + if !result && locking_enabled? + raise ActiveRecord::StaleObjectError.new(self, "touch") + end + + result else true end diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb index b6de90e89d..7e907beec0 100644 --- a/activerecord/lib/active_record/railtie.rb +++ b/activerecord/lib/active_record/railtie.rb @@ -58,9 +58,6 @@ module ActiveRecord require "active_record/railties/console_sandbox" if app.sandbox? require "active_record/base" console = ActiveSupport::Logger.new(STDERR) - console.formatter = Rails.logger.formatter - console.level = Rails.logger.level - Rails.logger.extend ActiveSupport::Logger.broadcast console end @@ -105,6 +102,14 @@ module ActiveRecord end end + initializer "active_record.warn_on_records_fetched_greater_than" do + if config.active_record.warn_on_records_fetched_greater_than + ActiveSupport.on_load(:active_record) do + require 'active_record/relation/record_fetch_warning' + end + end + end + initializer "active_record.set_configs" do |app| ActiveSupport.on_load(:active_record) do app.config.active_record.each do |k,v| diff --git a/activerecord/lib/active_record/railties/controller_runtime.rb b/activerecord/lib/active_record/railties/controller_runtime.rb index af4840476c..8727e46cb3 100644 --- a/activerecord/lib/active_record/railties/controller_runtime.rb +++ b/activerecord/lib/active_record/railties/controller_runtime.rb @@ -19,7 +19,7 @@ module ActiveRecord end def cleanup_view_runtime - if ActiveRecord::Base.connected? + if logger.info? && ActiveRecord::Base.connected? db_rt_before_render = ActiveRecord::LogSubscriber.reset_runtime self.db_runtime = (db_runtime || 0) + db_rt_before_render runtime = super diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index dab5a502a5..1b0ae2c942 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -166,8 +166,8 @@ module ActiveRecord # AggregateReflection and AssociationReflection are returned by the Reflection::ClassMethods. # # MacroReflection + # AggregateReflection # AssociationReflection - # AggregateReflection # HasManyReflection # HasOneReflection # BelongsToReflection @@ -196,7 +196,7 @@ module ActiveRecord @scope = scope @options = options @active_record = active_record - @klass = options[:class] + @klass = options[:anonymous_class] @plural_name = active_record.pluralize_table_names ? name.to_s.pluralize : name.to_s end @@ -370,6 +370,12 @@ module ActiveRecord [self] end + # This is for clearing cache on the reflection. Useful for tests that need to compare + # SQL queries on associations. + def clear_association_scope_cache # :nodoc: + @association_scope_cache.clear + end + def nested? false end @@ -629,7 +635,7 @@ module ActiveRecord def initialize(delegate_reflection) @delegate_reflection = delegate_reflection - @klass = delegate_reflection.options[:class] + @klass = delegate_reflection.options[:anonymous_class] @source_reflection_name = delegate_reflection.options[:source] end @@ -694,7 +700,7 @@ module ActiveRecord def chain @chain ||= begin a = source_reflection.chain - b = through_reflection.chain + b = through_reflection.chain.map(&:dup) if options[:source_type] b[0] = PolymorphicReflection.new(b[0], self) @@ -706,6 +712,15 @@ module ActiveRecord end end + # This is for clearing cache on the reflection. Useful for tests that need to compare + # SQL queries on associations. + def clear_association_scope_cache # :nodoc: + @chain = nil + delegate_reflection.clear_association_scope_cache + source_reflection.clear_association_scope_cache + through_reflection.clear_association_scope_cache + end + # Consider the following example: # # class Person diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index 6bbec7c0c0..85648a7f8f 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -205,7 +205,9 @@ module ActiveRecord # constraint an exception may be raised, just retry: # # begin - # CreditAccount.find_or_create_by(user_id: user.id) + # CreditAccount.transaction(requires_new: true) do + # CreditAccount.find_or_create_by(user_id: user.id) + # end # rescue ActiveRecord::RecordNotUnique # retry # end diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index 7f27e7b463..402b317d9c 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -130,9 +130,9 @@ module ActiveRecord # the plucked column names, if they can be deduced. Plucking an SQL fragment # returns String values by default. # - # Person.pluck(:id) - # # SELECT people.id FROM people - # # => [1, 2, 3] + # Person.pluck(:name) + # # SELECT people.name FROM people + # # => ['David', 'Jeremy', 'Jose'] # # Person.pluck(:id, :name) # # SELECT people.id, people.name FROM people @@ -150,6 +150,8 @@ module ActiveRecord # # SELECT DATEDIFF(updated_at, created_at) FROM people # # => ['0', '27761', '173'] # + # See also +ids+. + # def pluck(*column_names) column_names.map! do |column_name| if column_name.is_a?(Symbol) && attribute_alias?(column_name) @@ -182,7 +184,7 @@ module ActiveRecord private def has_include?(column_name) - eager_loading? || (includes_values.present? && ((column_name && column_name != :all) || references_eager_loaded_tables?)) + eager_loading? || (includes_values.present? && column_name && column_name != :all) end def perform_calculation(operation, column_name) diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index 7bd091b66c..6a3a56f1cc 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -5,7 +5,7 @@ module ActiveRecord ONE_AS_ONE = '1 AS one' # Find by id - This can either be a specific id (1), a list of ids (1, 5, 6), or an array of ids ([5, 6, 10]). - # If no record can be found for all of the listed ids, then RecordNotFound will be raised. If the primary key + # If one or more records can not be found for the requested ids, then RecordNotFound will be raised. If the primary key # is an integer, find by id coerces its arguments using +to_i+. # # Person.find(1) # returns the object for ID = 1 @@ -16,8 +16,6 @@ module ActiveRecord # Person.find([1]) # returns an array for the object with ID = 1 # Person.where("administrator = 1").order("created_on DESC").find(1) # - # <tt>ActiveRecord::RecordNotFound</tt> will be raised if one or more ids are not found. - # # NOTE: The returned records may not be in the same order as the ids you # provide since database rows are unordered. You'd need to provide an explicit <tt>order</tt> # option if you want the results are sorted. @@ -378,7 +376,7 @@ module ActiveRecord def construct_relation_for_association_calculations from = arel.froms.first if Arel::Table === from - apply_join_dependency(self, construct_join_dependency) + apply_join_dependency(self, construct_join_dependency(joins_values)) else # FIXME: as far as I can tell, `from` will always be an Arel::Table. # There are no tests that test this branch, but presumably it's diff --git a/activerecord/lib/active_record/relation/record_fetch_warning.rb b/activerecord/lib/active_record/relation/record_fetch_warning.rb new file mode 100644 index 0000000000..14e1bf89fa --- /dev/null +++ b/activerecord/lib/active_record/relation/record_fetch_warning.rb @@ -0,0 +1,49 @@ +module ActiveRecord + class Relation + module RecordFetchWarning + # When this module is prepended to ActiveRecord::Relation and + # `config.active_record.warn_on_records_fetched_greater_than` is + # set to an integer, if the number of records a query returns is + # greater than the value of `warn_on_records_fetched_greater_than`, + # a warning is logged. This allows for the detection of queries that + # return a large number of records, which could cause memory bloat. + # + # In most cases, fetching large number of records can be performed + # efficiently using the ActiveRecord::Batches methods. + # See active_record/lib/relation/batches.rb for more information. + def exec_queries + QueryRegistry.reset + + super.tap do + if logger && warn_on_records_fetched_greater_than + if @records.length > warn_on_records_fetched_greater_than + logger.warn "Query fetched #{@records.size} #{@klass} records: #{QueryRegistry.queries.join(";")}" + end + end + end + end + + ActiveSupport::Notifications.subscribe("sql.active_record") do |*args| + payload = args.last + + QueryRegistry.queries << payload[:sql] + end + + class QueryRegistry # :nodoc: + extend ActiveSupport::PerThreadRegistry + + attr_accessor :queries + + def initialize + reset + end + + def reset + @queries = [] + end + end + end + end +end + +ActiveRecord::Relation.prepend ActiveRecord::Relation::RecordFetchWarning diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb index da95920571..eaeaf0321b 100644 --- a/activerecord/lib/active_record/schema_dumper.rb +++ b/activerecord/lib/active_record/schema_dumper.rb @@ -105,7 +105,10 @@ HEADER end def table(table, stream) - columns = @connection.columns(table) + columns = @connection.columns(table).map do |column| + column.instance_variable_set(:@table_name, table) + column + end begin tbl = StringIO.new diff --git a/activerecord/lib/active_record/scoping.rb b/activerecord/lib/active_record/scoping.rb index fca4f1c9d3..f049b658c4 100644 --- a/activerecord/lib/active_record/scoping.rb +++ b/activerecord/lib/active_record/scoping.rb @@ -17,6 +17,17 @@ module ActiveRecord def current_scope=(scope) #:nodoc: ScopeRegistry.set_value_for(:current_scope, self.to_s, scope) end + + # Collects attributes from scopes that should be applied when creating + # an AR instance for the particular class this is called on. + def scope_attributes # :nodoc: + all.scope_for_create + end + + # Are there attributes associated with this scope? + def scope_attributes? # :nodoc: + current_scope + end end def populate_with_current_scope_attributes diff --git a/activerecord/lib/active_record/scoping/default.rb b/activerecord/lib/active_record/scoping/default.rb index 18190cb535..3590b8846e 100644 --- a/activerecord/lib/active_record/scoping/default.rb +++ b/activerecord/lib/active_record/scoping/default.rb @@ -33,6 +33,11 @@ module ActiveRecord block_given? ? relation.scoping { yield } : relation end + # Are there attributes associated with this scope? + def scope_attributes? # :nodoc: + super || default_scopes.any? || respond_to?(:default_scope) + end + def before_remove_const #:nodoc: self.current_scope = nil end diff --git a/activerecord/lib/active_record/scoping/named.rb b/activerecord/lib/active_record/scoping/named.rb index 43c7b1c574..7b62626896 100644 --- a/activerecord/lib/active_record/scoping/named.rb +++ b/activerecord/lib/active_record/scoping/named.rb @@ -39,17 +39,6 @@ module ActiveRecord end end - # Collects attributes from scopes that should be applied when creating - # an AR instance for the particular class this is called on. - def scope_attributes # :nodoc: - all.scope_for_create - end - - # Are there default attributes associated with this scope? - def scope_attributes? # :nodoc: - current_scope || default_scopes.any? - end - # Adds a class method for retrieving and querying objects. A \scope # represents a narrowing of a database query, such as # <tt>where(color: :red).select('shirts.*').includes(:washing_instructions)</tt>. diff --git a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb index ce1de4b76e..d7da95c8a9 100644 --- a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb +++ b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb @@ -46,7 +46,15 @@ module ActiveRecord def structure_dump(filename) set_psql_env - search_path = configuration['schema_search_path'] + + search_path = case ActiveRecord::Base.dump_schemas + when :schema_search_path + configuration['schema_search_path'] + when :all + nil + when String + ActiveRecord::Base.dump_schemas + end unless search_path.blank? search_path = search_path.split(",").map{|search_path_part| "--schema=#{Shellwords.escape(search_path_part.strip)}" }.join(" ") end @@ -59,7 +67,7 @@ module ActiveRecord def structure_load(filename) set_psql_env - Kernel.system("psql -q -f #{Shellwords.escape(filename)} #{configuration['database']}") + Kernel.system("psql -X -q -f #{Shellwords.escape(filename)} #{configuration['database']}") end private diff --git a/activerecord/lib/active_record/touch_later.rb b/activerecord/lib/active_record/touch_later.rb new file mode 100644 index 0000000000..4352a0ffea --- /dev/null +++ b/activerecord/lib/active_record/touch_later.rb @@ -0,0 +1,50 @@ +module ActiveRecord + # = Active Record Touch Later + module TouchLater + extend ActiveSupport::Concern + + included do + before_commit_without_transaction_enrollment :touch_deferred_attributes + end + + def touch_later(*names) # :nodoc: + raise ActiveRecordError, "cannot touch on a new record object" unless persisted? + + @_defer_touch_attrs ||= timestamp_attributes_for_update_in_model + @_defer_touch_attrs |= names + @_touch_time = current_time_from_proper_timezone + + surreptitiously_touch @_defer_touch_attrs + self.class.connection.add_transaction_record self + end + + def touch(*names, time: nil) # :nodoc: + if has_defer_touch_attrs? + names |= @_defer_touch_attrs + end + super(*names, time: time) + end + + private + def surreptitiously_touch(attrs) + attrs.each { |attr| write_attribute attr, @_touch_time } + clear_attribute_changes attrs + end + + def touch_deferred_attributes + if has_defer_touch_attrs? && persisted? + @_touching_delayed_records = true + touch(*@_defer_touch_attrs, time: @_touch_time) + @_touching_delayed_records, @_defer_touch_attrs, @_touch_time = nil, nil, nil + end + end + + def has_defer_touch_attrs? + defined?(@_defer_touch_attrs) && @_defer_touch_attrs.present? + end + + def touching_delayed_records? + defined?(@_touching_delayed_records) && @_touching_delayed_records + end + end +end diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb index ffe7c4ae42..311dacb449 100644 --- a/activerecord/lib/active_record/transactions.rb +++ b/activerecord/lib/active_record/transactions.rb @@ -227,8 +227,6 @@ module ActiveRecord # after_commit :do_foo_bar, on: [:create, :update] # after_commit :do_bar_baz, on: [:update, :destroy] # - # Note that transactional fixtures do not play well with this feature. Please - # use the +test_after_commit+ gem to have these hooks fired in tests. def after_commit(*args, &block) set_options_for_callbacks!(args) set_callback(:commit, :after, *args, &block) @@ -321,8 +319,8 @@ module ActiveRecord end def before_committed! # :nodoc: - _run_before_commit_without_transaction_enrollment_callbacks - _run_before_commit_callbacks + run_callbacks :before_commit_without_transaction_enrollment + run_callbacks :before_commit end # Call the +after_commit+ callbacks. @@ -331,8 +329,8 @@ module ActiveRecord # but call it after the commit of a destroyed object. def committed!(should_run_callbacks: true) #:nodoc: if should_run_callbacks && destroyed? || persisted? - _run_commit_without_transaction_enrollment_callbacks - _run_commit_callbacks + run_callbacks :commit_without_transaction_enrollment + run_callbacks :commit end ensure force_clear_transaction_record_state @@ -342,8 +340,8 @@ module ActiveRecord # state should be rolled back to the beginning or just to the last savepoint. def rolledback!(force_restore_state: false, should_run_callbacks: true) #:nodoc: if should_run_callbacks - _run_rollback_without_transaction_enrollment_callbacks - _run_rollback_callbacks + run_callbacks :rollback + run_callbacks :rollback_without_transaction_enrollment end ensure restore_transaction_record_state(force_restore_state) @@ -356,6 +354,7 @@ module ActiveRecord if has_transactional_callbacks? self.class.connection.add_transaction_record(self) else + sync_with_transaction_state set_transaction_state(self.class.connection.transaction_state) end remember_transaction_record_state @@ -413,13 +412,14 @@ module ActiveRecord transaction_level = (@_start_transaction_state[:level] || 0) - 1 if transaction_level < 1 || force restore_state = @_start_transaction_state - thaw unless restore_state[:frozen?] + thaw @new_record = restore_state[:new_record] @destroyed = restore_state[:destroyed] pk = self.class.primary_key if pk && read_attribute(pk) != restore_state[:id] write_attribute(pk, restore_state[:id]) end + freeze if restore_state[:frozen?] end end end @@ -454,13 +454,13 @@ module ActiveRecord end # Updates the attributes on this particular ActiveRecord object so that - # if it is associated with a transaction, then the state of the AR object - # will be updated to reflect the current state of the transaction + # if it's associated with a transaction, then the state of the ActiveRecord + # object will be updated to reflect the current state of the transaction # # The @transaction_state variable stores the states of the associated # transaction. This relies on the fact that a transaction can only be in # one rollback or commit (otherwise a list of states would be required) - # Each AR object inside of a transaction carries that transaction's + # Each ActiveRecord object inside of a transaction carries that transaction's # TransactionState. # # This method checks to see if the ActiveRecord object's state reflects diff --git a/activerecord/lib/active_record/type/hash_lookup_type_map.rb b/activerecord/lib/active_record/type/hash_lookup_type_map.rb index 82d9327fc0..3b01e3f8ca 100644 --- a/activerecord/lib/active_record/type/hash_lookup_type_map.rb +++ b/activerecord/lib/active_record/type/hash_lookup_type_map.rb @@ -1,12 +1,18 @@ module ActiveRecord module Type class HashLookupTypeMap < TypeMap # :nodoc: - delegate :key?, to: :@mapping - def alias_type(type, alias_type) register_type(type) { |_, *args| lookup(alias_type, *args) } end + def key?(key) + @mapping.key?(key) + end + + def keys + @mapping.keys + end + private def perform_fetch(type, *args, &block) diff --git a/activerecord/lib/active_record/type/serialized.rb b/activerecord/lib/active_record/type/serialized.rb index 732029c723..ea3e0d6a45 100644 --- a/activerecord/lib/active_record/type/serialized.rb +++ b/activerecord/lib/active_record/type/serialized.rb @@ -26,9 +26,15 @@ module ActiveRecord end end + def inspect + Kernel.instance_method(:inspect).bind(self).call + end + def changed_in_place?(raw_old_value, value) return false if value.nil? - subtype.changed_in_place?(raw_old_value, serialize(value)) + raw_new_value = serialize(value) + raw_old_value.nil? != raw_new_value.nil? || + subtype.changed_in_place?(raw_old_value, raw_new_value) end def accessor diff --git a/activerecord/lib/active_record/validations/presence.rb b/activerecord/lib/active_record/validations/presence.rb index 75d5bd5a35..a9b791397b 100644 --- a/activerecord/lib/active_record/validations/presence.rb +++ b/activerecord/lib/active_record/validations/presence.rb @@ -43,6 +43,10 @@ module ActiveRecord # deletes the associated object, thus putting the parent object into an invalid # state. # + # NOTE: This validation will not fail while using it with an association + # if the latter was assigned but not valid. If you want to ensure that + # it is both present and valid, you also need to use +validates_associated+. + # # Configuration options: # * <tt>:message</tt> - A custom error message (default is: "can't be blank"). # * <tt>:on</tt> - Specifies the contexts where this validation is active. diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb index 9be4b10a55..5106f4e127 100644 --- a/activerecord/lib/active_record/validations/uniqueness.rb +++ b/activerecord/lib/active_record/validations/uniqueness.rb @@ -76,6 +76,8 @@ module ActiveRecord klass.connection.case_sensitive_comparison(table, attribute, column, value) end klass.unscoped.where(comparison) + rescue RangeError + klass.none end def scope_relation(record, table, relation) diff --git a/activerecord/test/cases/adapter_test.rb b/activerecord/test/cases/adapter_test.rb index ecf1368a91..1712ff0ac6 100644 --- a/activerecord/test/cases/adapter_test.rb +++ b/activerecord/test/cases/adapter_test.rb @@ -226,7 +226,7 @@ module ActiveRecord end class AdapterTestWithoutTransaction < ActiveRecord::TestCase - self.use_transactional_fixtures = false + self.use_transactional_tests = false class Klass < ActiveRecord::Base end diff --git a/activerecord/test/cases/adapters/mysql/charset_collation_test.rb b/activerecord/test/cases/adapters/mysql/charset_collation_test.rb new file mode 100644 index 0000000000..c8dd49d00a --- /dev/null +++ b/activerecord/test/cases/adapters/mysql/charset_collation_test.rb @@ -0,0 +1,54 @@ +require "cases/helper" +require 'support/schema_dumping_helper' + +class CharsetCollationTest < ActiveRecord::TestCase + include SchemaDumpingHelper + self.use_transactional_tests = false + + setup do + @connection = ActiveRecord::Base.connection + @connection.create_table :charset_collations, force: true do |t| + t.string :string_ascii_bin, charset: 'ascii', collation: 'ascii_bin' + t.text :text_ucs2_unicode_ci, charset: 'ucs2', collation: 'ucs2_unicode_ci' + end + end + + teardown do + @connection.drop_table :charset_collations, if_exists: true + end + + test "string column with charset and collation" do + column = @connection.columns(:charset_collations).find { |c| c.name == 'string_ascii_bin' } + assert_equal :string, column.type + assert_equal 'ascii_bin', column.collation + end + + test "text column with charset and collation" do + column = @connection.columns(:charset_collations).find { |c| c.name == 'text_ucs2_unicode_ci' } + assert_equal :text, column.type + assert_equal 'ucs2_unicode_ci', column.collation + end + + test "add column with charset and collation" do + @connection.add_column :charset_collations, :title, :string, charset: 'utf8', collation: 'utf8_bin' + + column = @connection.columns(:charset_collations).find { |c| c.name == 'title' } + assert_equal :string, column.type + assert_equal 'utf8_bin', column.collation + end + + test "change column with charset and collation" do + @connection.add_column :charset_collations, :description, :string, charset: 'utf8', collation: 'utf8_unicode_ci' + @connection.change_column :charset_collations, :description, :text, charset: 'utf8', collation: 'utf8_general_ci' + + column = @connection.columns(:charset_collations).find { |c| c.name == 'description' } + assert_equal :text, column.type + assert_equal 'utf8_general_ci', column.collation + end + + test "schema dump includes collation" do + output = dump_table_schema("charset_collations") + assert_match %r{t.string\s+"string_ascii_bin",\s+limit: 255,\s+collation: "ascii_bin"$}, output + assert_match %r{t.text\s+"text_ucs2_unicode_ci",\s+limit: 65535,\s+collation: "ucs2_unicode_ci"$}, output + end +end diff --git a/activerecord/test/cases/adapters/mysql/consistency_test.rb b/activerecord/test/cases/adapters/mysql/consistency_test.rb index e972d6b330..ae190b728d 100644 --- a/activerecord/test/cases/adapters/mysql/consistency_test.rb +++ b/activerecord/test/cases/adapters/mysql/consistency_test.rb @@ -1,7 +1,7 @@ require "cases/helper" class MysqlConsistencyTest < ActiveRecord::TestCase - self.use_transactional_fixtures = false + self.use_transactional_tests = false class Consistency < ActiveRecord::Base self.table_name = "mysql_consistency" diff --git a/activerecord/test/cases/adapters/mysql/reserved_word_test.rb b/activerecord/test/cases/adapters/mysql/reserved_word_test.rb index 2f9c070255..ec1c394f40 100644 --- a/activerecord/test/cases/adapters/mysql/reserved_word_test.rb +++ b/activerecord/test/cases/adapters/mysql/reserved_word_test.rb @@ -71,7 +71,7 @@ class MysqlReservedWordTest < ActiveRecord::TestCase #fixtures self.use_instantiated_fixtures = true - self.use_transactional_fixtures = false + self.use_transactional_tests = false #activerecord model class with reserved-word table name def test_activerecord_model diff --git a/activerecord/test/cases/adapters/mysql/unsigned_type_test.rb b/activerecord/test/cases/adapters/mysql/unsigned_type_test.rb index 8f521e9181..e9edc53f93 100644 --- a/activerecord/test/cases/adapters/mysql/unsigned_type_test.rb +++ b/activerecord/test/cases/adapters/mysql/unsigned_type_test.rb @@ -1,7 +1,7 @@ require "cases/helper" class UnsignedTypeTest < ActiveRecord::TestCase - self.use_transactional_fixtures = false + self.use_transactional_tests = false class UnsignedType < ActiveRecord::Base end diff --git a/activerecord/test/cases/adapters/mysql2/boolean_test.rb b/activerecord/test/cases/adapters/mysql2/boolean_test.rb index 0e641ba3bf..0d81dd6eee 100644 --- a/activerecord/test/cases/adapters/mysql2/boolean_test.rb +++ b/activerecord/test/cases/adapters/mysql2/boolean_test.rb @@ -1,7 +1,7 @@ require "cases/helper" class Mysql2BooleanTest < ActiveRecord::TestCase - self.use_transactional_fixtures = false + self.use_transactional_tests = false class BooleanType < ActiveRecord::Base self.table_name = "mysql_booleans" diff --git a/activerecord/test/cases/adapters/mysql2/charset_collation_test.rb b/activerecord/test/cases/adapters/mysql2/charset_collation_test.rb new file mode 100644 index 0000000000..c8dd49d00a --- /dev/null +++ b/activerecord/test/cases/adapters/mysql2/charset_collation_test.rb @@ -0,0 +1,54 @@ +require "cases/helper" +require 'support/schema_dumping_helper' + +class CharsetCollationTest < ActiveRecord::TestCase + include SchemaDumpingHelper + self.use_transactional_tests = false + + setup do + @connection = ActiveRecord::Base.connection + @connection.create_table :charset_collations, force: true do |t| + t.string :string_ascii_bin, charset: 'ascii', collation: 'ascii_bin' + t.text :text_ucs2_unicode_ci, charset: 'ucs2', collation: 'ucs2_unicode_ci' + end + end + + teardown do + @connection.drop_table :charset_collations, if_exists: true + end + + test "string column with charset and collation" do + column = @connection.columns(:charset_collations).find { |c| c.name == 'string_ascii_bin' } + assert_equal :string, column.type + assert_equal 'ascii_bin', column.collation + end + + test "text column with charset and collation" do + column = @connection.columns(:charset_collations).find { |c| c.name == 'text_ucs2_unicode_ci' } + assert_equal :text, column.type + assert_equal 'ucs2_unicode_ci', column.collation + end + + test "add column with charset and collation" do + @connection.add_column :charset_collations, :title, :string, charset: 'utf8', collation: 'utf8_bin' + + column = @connection.columns(:charset_collations).find { |c| c.name == 'title' } + assert_equal :string, column.type + assert_equal 'utf8_bin', column.collation + end + + test "change column with charset and collation" do + @connection.add_column :charset_collations, :description, :string, charset: 'utf8', collation: 'utf8_unicode_ci' + @connection.change_column :charset_collations, :description, :text, charset: 'utf8', collation: 'utf8_general_ci' + + column = @connection.columns(:charset_collations).find { |c| c.name == 'description' } + assert_equal :text, column.type + assert_equal 'utf8_general_ci', column.collation + end + + test "schema dump includes collation" do + output = dump_table_schema("charset_collations") + assert_match %r{t.string\s+"string_ascii_bin",\s+limit: 255,\s+collation: "ascii_bin"$}, output + assert_match %r{t.text\s+"text_ucs2_unicode_ci",\s+limit: 65535,\s+collation: "ucs2_unicode_ci"$}, output + end +end diff --git a/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb b/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb index beb829fc46..799e60a683 100644 --- a/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb +++ b/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb @@ -71,7 +71,7 @@ class MysqlReservedWordTest < ActiveRecord::TestCase #fixtures self.use_instantiated_fixtures = true - self.use_transactional_fixtures = false + self.use_transactional_tests = false #activerecord model class with reserved-word table name def test_activerecord_model diff --git a/activerecord/test/cases/adapters/mysql2/unsigned_type_test.rb b/activerecord/test/cases/adapters/mysql2/unsigned_type_test.rb index 8f521e9181..e9edc53f93 100644 --- a/activerecord/test/cases/adapters/mysql2/unsigned_type_test.rb +++ b/activerecord/test/cases/adapters/mysql2/unsigned_type_test.rb @@ -1,7 +1,7 @@ require "cases/helper" class UnsignedTypeTest < ActiveRecord::TestCase - self.use_transactional_fixtures = false + self.use_transactional_tests = false class UnsignedType < ActiveRecord::Base end diff --git a/activerecord/test/cases/adapters/postgresql/array_test.rb b/activerecord/test/cases/adapters/postgresql/array_test.rb index 2163e35e70..6edbd9c3a6 100644 --- a/activerecord/test/cases/adapters/postgresql/array_test.rb +++ b/activerecord/test/cases/adapters/postgresql/array_test.rb @@ -23,6 +23,7 @@ class PostgresqlArrayTest < ActiveRecord::TestCase t.hstore :hstores, array: true end end + PgArray.reset_column_information @column = PgArray.columns_hash['tags'] @type = PgArray.type_for_attribute("tags") end diff --git a/activerecord/test/cases/adapters/postgresql/datatype_test.rb b/activerecord/test/cases/adapters/postgresql/datatype_test.rb index 4f48a7bce3..2c14252ae4 100644 --- a/activerecord/test/cases/adapters/postgresql/datatype_test.rb +++ b/activerecord/test/cases/adapters/postgresql/datatype_test.rb @@ -12,7 +12,7 @@ class PostgresqlLtree < ActiveRecord::Base end class PostgresqlDataTypeTest < ActiveRecord::TestCase - self.use_transactional_fixtures = false + self.use_transactional_tests = false def setup @connection = ActiveRecord::Base.connection diff --git a/activerecord/test/cases/adapters/postgresql/enum_test.rb b/activerecord/test/cases/adapters/postgresql/enum_test.rb index 7458de23d8..ed084483bc 100644 --- a/activerecord/test/cases/adapters/postgresql/enum_test.rb +++ b/activerecord/test/cases/adapters/postgresql/enum_test.rb @@ -80,4 +80,12 @@ class PostgresqlEnumTest < ActiveRecord::TestCase assert_equal "happy", enum.current_mood end + + def test_assigning_enum_to_nil + model = PostgresqlEnum.new(current_mood: nil) + + assert_nil model.current_mood + assert model.save + assert_nil model.reload.current_mood + end end diff --git a/activerecord/test/cases/adapters/postgresql/extension_migration_test.rb b/activerecord/test/cases/adapters/postgresql/extension_migration_test.rb index 7b99fcdda0..06d427f464 100644 --- a/activerecord/test/cases/adapters/postgresql/extension_migration_test.rb +++ b/activerecord/test/cases/adapters/postgresql/extension_migration_test.rb @@ -1,7 +1,7 @@ require "cases/helper" class PostgresqlExtensionMigrationTest < ActiveRecord::TestCase - self.use_transactional_fixtures = false + self.use_transactional_tests = false class EnableHstore < ActiveRecord::Migration def change diff --git a/activerecord/test/cases/adapters/postgresql/hstore_test.rb b/activerecord/test/cases/adapters/postgresql/hstore_test.rb index e6835031c3..ad9dd311a6 100644 --- a/activerecord/test/cases/adapters/postgresql/hstore_test.rb +++ b/activerecord/test/cases/adapters/postgresql/hstore_test.rb @@ -27,6 +27,7 @@ if ActiveRecord::Base.connection.supports_extensions? t.hstore 'settings' end end + Hstore.reset_column_information @column = Hstore.columns_hash['tags'] @type = Hstore.type_for_attribute("tags") end diff --git a/activerecord/test/cases/adapters/postgresql/infinity_test.rb b/activerecord/test/cases/adapters/postgresql/infinity_test.rb index 24199c69b8..d9d7832094 100644 --- a/activerecord/test/cases/adapters/postgresql/infinity_test.rb +++ b/activerecord/test/cases/adapters/postgresql/infinity_test.rb @@ -24,6 +24,15 @@ class PostgresqlInfinityTest < ActiveRecord::TestCase assert_equal Float::INFINITY, record.float end + test "type casting string on a float column" do + record = PostgresqlInfinity.new(float: 'Infinity') + assert_equal Float::INFINITY, record.float + record = PostgresqlInfinity.new(float: '-Infinity') + assert_equal(-Float::INFINITY, record.float) + record = PostgresqlInfinity.new(float: 'NaN') + assert_send [record.float, :nan?] + end + test "update_all with infinity on a float column" do record = PostgresqlInfinity.create! PostgresqlInfinity.update_all(float: Float::INFINITY) diff --git a/activerecord/test/cases/adapters/postgresql/json_test.rb b/activerecord/test/cases/adapters/postgresql/json_test.rb index 21c409c62d..6878516aeb 100644 --- a/activerecord/test/cases/adapters/postgresql/json_test.rb +++ b/activerecord/test/cases/adapters/postgresql/json_test.rb @@ -25,6 +25,7 @@ module PostgresqlJSONSharedTestCases def teardown @connection.drop_table :json_data_type, if_exists: true + JsonDataType.reset_column_information end def test_column diff --git a/activerecord/test/cases/adapters/postgresql/numbers_test.rb b/activerecord/test/cases/adapters/postgresql/numbers_test.rb index 093b81fe8d..d8e01e3b89 100644 --- a/activerecord/test/cases/adapters/postgresql/numbers_test.rb +++ b/activerecord/test/cases/adapters/postgresql/numbers_test.rb @@ -31,7 +31,7 @@ class PostgresqlNumberTest < ActiveRecord::TestCase assert_equal 123456.789, first.double assert_equal(-::Float::INFINITY, second.single) assert_equal ::Float::INFINITY, second.double - assert_same ::Float::NAN, third.double + assert_send [third.double, :nan?] end def test_update diff --git a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb index a934180a43..9a1b889d4d 100644 --- a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb +++ b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb @@ -68,7 +68,7 @@ module ActiveRecord def test_insert_sql_with_proprietary_returning_clause with_example_table do id = @connection.insert_sql("insert into ex (number) values(5150)", nil, "number") - assert_equal "5150", id + assert_equal 5150, id end end @@ -106,21 +106,21 @@ module ActiveRecord connection = connection_without_insert_returning id = connection.insert_sql("insert into postgresql_partitioned_table_parent (number) VALUES (1)") expect = connection.query('select max(id) from postgresql_partitioned_table_parent').first.first - assert_equal expect, id + assert_equal expect.to_i, id end def test_exec_insert_with_returning_disabled connection = connection_without_insert_returning result = connection.exec_insert("insert into postgresql_partitioned_table_parent (number) VALUES (1)", nil, [], 'id', 'postgresql_partitioned_table_parent_id_seq') expect = connection.query('select max(id) from postgresql_partitioned_table_parent').first.first - assert_equal expect, result.rows.first.first + assert_equal expect.to_i, result.rows.first.first end def test_exec_insert_with_returning_disabled_and_no_sequence_name_given connection = connection_without_insert_returning result = connection.exec_insert("insert into postgresql_partitioned_table_parent (number) VALUES (1)", nil, [], 'id') expect = connection.query('select max(id) from postgresql_partitioned_table_parent').first.first - assert_equal expect, result.rows.first.first + assert_equal expect.to_i, result.rows.first.first end def test_sql_for_insert_with_returning_disabled @@ -238,7 +238,7 @@ module ActiveRecord result = @connection.exec_query('SELECT number FROM ex WHERE number = 10') assert_equal 1, result.rows.length - assert_equal "10", result.rows.last.last + assert_equal 10, result.rows.last.last end end @@ -274,7 +274,7 @@ module ActiveRecord assert_equal 1, result.rows.length assert_equal 2, result.columns.length - assert_equal [['1', 'foo']], result.rows + assert_equal [[1, 'foo']], result.rows end end @@ -288,7 +288,7 @@ module ActiveRecord assert_equal 1, result.rows.length assert_equal 2, result.columns.length - assert_equal [['1', 'foo']], result.rows + assert_equal [[1, 'foo']], result.rows end end @@ -304,7 +304,7 @@ module ActiveRecord assert_equal 1, result.rows.length assert_equal 2, result.columns.length - assert_equal [['1', 'foo']], result.rows + assert_equal [[1, 'foo']], result.rows end end diff --git a/activerecord/test/cases/adapters/postgresql/range_test.rb b/activerecord/test/cases/adapters/postgresql/range_test.rb index b6b451ca5c..bbf96278b0 100644 --- a/activerecord/test/cases/adapters/postgresql/range_test.rb +++ b/activerecord/test/cases/adapters/postgresql/range_test.rb @@ -7,7 +7,7 @@ if ActiveRecord::Base.connection.supports_ranges? end class PostgresqlRangeTest < ActiveRecord::TestCase - self.use_transactional_fixtures = false + self.use_transactional_tests = false include ConnectionHelper def setup diff --git a/activerecord/test/cases/adapters/postgresql/referential_integrity_test.rb b/activerecord/test/cases/adapters/postgresql/referential_integrity_test.rb index 98291f1bbf..7200ed2771 100644 --- a/activerecord/test/cases/adapters/postgresql/referential_integrity_test.rb +++ b/activerecord/test/cases/adapters/postgresql/referential_integrity_test.rb @@ -2,13 +2,17 @@ require 'cases/helper' require 'support/connection_helper' class PostgreSQLReferentialIntegrityTest < ActiveRecord::TestCase - self.use_transactional_fixtures = false + self.use_transactional_tests = false include ConnectionHelper + IS_REFERENTIAL_INTEGRITY_SQL = lambda do |sql| + sql.match(/DISABLE TRIGGER ALL/) || sql.match(/ENABLE TRIGGER ALL/) + end + module MissingSuperuserPrivileges def execute(sql) - if sql.match(/DISABLE TRIGGER ALL/) || sql.match(/ENABLE TRIGGER ALL/) + if IS_REFERENTIAL_INTEGRITY_SQL.call(sql) super "BROKEN;" rescue nil # put transaction in broken state raise ActiveRecord::StatementInvalid, 'PG::InsufficientPrivilege' else @@ -17,6 +21,16 @@ class PostgreSQLReferentialIntegrityTest < ActiveRecord::TestCase end end + module ProgrammerMistake + def execute(sql) + if IS_REFERENTIAL_INTEGRITY_SQL.call(sql) + raise ArgumentError, 'something is not right.' + else + super + end + end + end + def setup @connection = ActiveRecord::Base.connection end @@ -81,9 +95,17 @@ class PostgreSQLReferentialIntegrityTest < ActiveRecord::TestCase end end + def test_only_catch_active_record_errors_others_bubble_up + @connection.extend ProgrammerMistake + + assert_raises ArgumentError do + @connection.disable_referential_integrity {} + end + end + private def assert_transaction_is_not_broken - assert_equal "1", @connection.select_value("SELECT 1") + assert_equal 1, @connection.select_value("SELECT 1") end end diff --git a/activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb b/activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb index 6937145439..359a45bbd1 100644 --- a/activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb +++ b/activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb @@ -4,7 +4,7 @@ class SchemaThing < ActiveRecord::Base end class SchemaAuthorizationTest < ActiveRecord::TestCase - self.use_transactional_fixtures = false + self.use_transactional_tests = false TABLE_NAME = 'schema_things' COLUMNS = [ diff --git a/activerecord/test/cases/adapters/postgresql/schema_test.rb b/activerecord/test/cases/adapters/postgresql/schema_test.rb index 77ff6d01bc..f925dcad97 100644 --- a/activerecord/test/cases/adapters/postgresql/schema_test.rb +++ b/activerecord/test/cases/adapters/postgresql/schema_test.rb @@ -3,7 +3,7 @@ require 'models/default' require 'support/schema_dumping_helper' class SchemaTest < ActiveRecord::TestCase - self.use_transactional_fixtures = false + self.use_transactional_tests = false SCHEMA_NAME = 'test_schema' SCHEMA2_NAME = 'test_schema2' @@ -384,16 +384,16 @@ class SchemaTest < ActiveRecord::TestCase def test_reset_pk_sequence sequence_name = "#{SCHEMA_NAME}.#{UNMATCHED_SEQUENCE_NAME}" @connection.execute "SELECT setval('#{sequence_name}', 123)" - assert_equal "124", @connection.select_value("SELECT nextval('#{sequence_name}')") + assert_equal 124, @connection.select_value("SELECT nextval('#{sequence_name}')") @connection.reset_pk_sequence!("#{SCHEMA_NAME}.#{UNMATCHED_PK_TABLE_NAME}") - assert_equal "1", @connection.select_value("SELECT nextval('#{sequence_name}')") + assert_equal 1, @connection.select_value("SELECT nextval('#{sequence_name}')") end def test_set_pk_sequence table_name = "#{SCHEMA_NAME}.#{PK_TABLE_NAME}" _, sequence_name = @connection.pk_and_sequence_for table_name @connection.set_pk_sequence! table_name, 123 - assert_equal "124", @connection.select_value("SELECT nextval('#{sequence_name}')") + assert_equal 124, @connection.select_value("SELECT nextval('#{sequence_name}')") @connection.reset_pk_sequence! table_name end diff --git a/activerecord/test/cases/adapters/postgresql/timestamp_test.rb b/activerecord/test/cases/adapters/postgresql/timestamp_test.rb index da14063e20..a639f98272 100644 --- a/activerecord/test/cases/adapters/postgresql/timestamp_test.rb +++ b/activerecord/test/cases/adapters/postgresql/timestamp_test.rb @@ -5,7 +5,7 @@ require 'models/topic' class PostgresqlTimestampTest < ActiveRecord::TestCase class PostgresqlTimestampWithZone < ActiveRecord::Base; end - self.use_transactional_fixtures = false + self.use_transactional_tests = false setup do @connection = ActiveRecord::Base.connection diff --git a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb index 5ca3c92027..27f4ba8eb6 100644 --- a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb @@ -8,7 +8,7 @@ module ActiveRecord class SQLite3AdapterTest < ActiveRecord::TestCase include DdlHelper - self.use_transactional_fixtures = false + self.use_transactional_tests = false class DualEncoding < ActiveRecord::Base end diff --git a/activerecord/test/cases/ar_schema_test.rb b/activerecord/test/cases/ar_schema_test.rb index f4e8003bc3..9d5327bf35 100644 --- a/activerecord/test/cases/ar_schema_test.rb +++ b/activerecord/test/cases/ar_schema_test.rb @@ -3,7 +3,7 @@ require "cases/helper" if ActiveRecord::Base.connection.supports_migrations? class ActiveRecordSchemaTest < ActiveRecord::TestCase - self.use_transactional_fixtures = false + self.use_transactional_tests = false setup do @original_verbose = ActiveRecord::Migration.verbose diff --git a/activerecord/test/cases/associations/belongs_to_associations_test.rb b/activerecord/test/cases/associations/belongs_to_associations_test.rb index 47fd7345c8..ba90c61d65 100644 --- a/activerecord/test/cases/associations/belongs_to_associations_test.rb +++ b/activerecord/test/cases/associations/belongs_to_associations_test.rb @@ -124,9 +124,9 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase where("id = :inc", :inc => counter) } - has_many :comments, :class => comments + has_many :comments, :anonymous_class => comments } - belongs_to :post, :class => posts, :inverse_of => false + belongs_to :post, :anonymous_class => posts, :inverse_of => false } assert_equal 0, counter @@ -422,6 +422,24 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase assert_queries(1) { line_item.touch } end + def test_belongs_to_with_touch_on_multiple_records + line_item = LineItem.create!(amount: 1) + line_item2 = LineItem.create!(amount: 2) + Invoice.create!(line_items: [line_item, line_item2]) + + assert_queries(1) do + LineItem.transaction do + line_item.touch + line_item2.touch + end + end + + assert_queries(2) do + line_item.touch + line_item2.touch + end + end + def test_belongs_to_with_touch_option_on_touch_without_updated_at_attributes assert_not LineItem.column_names.include?("updated_at") diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb index 7d8b933992..0ecf2ddfd1 100644 --- a/activerecord/test/cases/associations/eager_test.rb +++ b/activerecord/test/cases/associations/eager_test.rb @@ -759,6 +759,23 @@ class EagerAssociationTest < ActiveRecord::TestCase end end + def test_eager_with_default_scope_as_class_method_using_find_method + david = developers(:david) + developer = EagerDeveloperWithClassMethodDefaultScope.find(david.id) + projects = Project.order(:id).to_a + assert_no_queries do + assert_equal(projects, developer.projects) + end + end + + def test_eager_with_default_scope_as_class_method_using_find_by_method + developer = EagerDeveloperWithClassMethodDefaultScope.find_by(name: 'David') + projects = Project.order(:id).to_a + assert_no_queries do + assert_equal(projects, developer.projects) + end + end + def test_eager_with_default_scope_as_lambda developer = EagerDeveloperWithLambdaDefaultScope.where(:name => 'David').first projects = Project.order(:id).to_a diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index 675bed9bfa..14cdf37f46 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -119,9 +119,9 @@ class HasManyAssociationsTest < ActiveRecord::TestCase developer_project = Class.new(ActiveRecord::Base) { self.table_name = 'developers_projects' - belongs_to :developer, :class => dev + belongs_to :developer, :anonymous_class => dev } - has_many :developer_projects, :class => developer_project, :foreign_key => 'developer_id' + has_many :developer_projects, :anonymous_class => developer_project, :foreign_key => 'developer_id' } dev = developer.first named = Developer.find(dev.id) @@ -140,13 +140,13 @@ class HasManyAssociationsTest < ActiveRecord::TestCase comments = Class.new(ActiveRecord::Base) { self.table_name = 'comments' self.inheritance_column = 'not_there' - belongs_to :post, :class => post + belongs_to :post, :anonymous_class => post default_scope -> { counter += 1 where("id = :inc", :inc => counter) } } - has_many :comments, :class => comments, :foreign_key => 'post_id' + has_many :comments, :anonymous_class => comments, :foreign_key => 'post_id' } assert_equal 0, counter post = posts.first @@ -532,9 +532,21 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_update_all_on_association_accessed_before_save firm = Firm.new(name: 'Firm') + clients_proxy_id = firm.clients.object_id firm.clients << Client.first firm.save! assert_equal firm.clients.count, firm.clients.update_all(description: 'Great!') + assert_not_equal clients_proxy_id, firm.clients.object_id + end + + def test_update_all_on_association_accessed_before_save_with_explicit_foreign_key + # We can use the same cached proxy object because the id is available for the scope + firm = Firm.new(name: 'Firm', id: 100) + clients_proxy_id = firm.clients.object_id + firm.clients << Client.first + firm.save! + assert_equal firm.clients.count, firm.clients.update_all(description: 'Great!') + assert_equal clients_proxy_id, firm.clients.object_id end def test_belongs_to_sanity @@ -1491,6 +1503,8 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_queries(0, ignore_none: true) do firm.clients = [] end + + assert_equal [], firm.send('clients=', []) end def test_transactions_when_replacing_on_persisted 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 5f52c65412..9734ea2217 100644 --- a/activerecord/test/cases/associations/has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb @@ -84,11 +84,11 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase subscriber = make_model "Subscriber" subscriber.primary_key = 'nick' - subscription.belongs_to :book, class: book - subscription.belongs_to :subscriber, class: subscriber + subscription.belongs_to :book, anonymous_class: book + subscription.belongs_to :subscriber, anonymous_class: subscriber - book.has_many :subscriptions, class: subscription - book.has_many :subscribers, through: :subscriptions, class: subscriber + book.has_many :subscriptions, anonymous_class: subscription + book.has_many :subscribers, through: :subscriptions, anonymous_class: subscriber anonbook = book.first namebook = Book.find anonbook.id @@ -154,10 +154,10 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase lesson_student = make_model 'LessonStudent' lesson_student.table_name = 'lessons_students' - lesson_student.belongs_to :lesson, :class => lesson - lesson_student.belongs_to :student, :class => student - lesson.has_many :lesson_students, :class => lesson_student - lesson.has_many :students, :through => :lesson_students, :class => student + lesson_student.belongs_to :lesson, :anonymous_class => lesson + lesson_student.belongs_to :student, :anonymous_class => student + lesson.has_many :lesson_students, :anonymous_class => lesson_student + lesson.has_many :students, :through => :lesson_students, :anonymous_class => student [lesson, lesson_student, student] end diff --git a/activerecord/test/cases/associations/has_one_associations_test.rb b/activerecord/test/cases/associations/has_one_associations_test.rb index 7dc9266074..5c2e5e7b43 100644 --- a/activerecord/test/cases/associations/has_one_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_associations_test.rb @@ -12,7 +12,7 @@ require 'models/image' require 'models/post' class HasOneAssociationsTest < ActiveRecord::TestCase - self.use_transactional_fixtures = false unless supports_savepoints? + self.use_transactional_tests = false unless supports_savepoints? fixtures :accounts, :companies, :developers, :projects, :developers_projects, :ships, :pirates def setup diff --git a/activerecord/test/cases/associations/join_model_test.rb b/activerecord/test/cases/associations/join_model_test.rb index 9918601623..213be50e67 100644 --- a/activerecord/test/cases/associations/join_model_test.rb +++ b/activerecord/test/cases/associations/join_model_test.rb @@ -17,7 +17,7 @@ require 'models/engine' require 'models/car' class AssociationsJoinModelTest < ActiveRecord::TestCase - self.use_transactional_fixtures = false unless supports_savepoints? + self.use_transactional_tests = false unless supports_savepoints? fixtures :posts, :authors, :categories, :categorizations, :comments, :tags, :taggings, :author_favorites, :vertices, :items, :books, # Reload edges table from fixtures as otherwise repeated test was failing diff --git a/activerecord/test/cases/associations/required_test.rb b/activerecord/test/cases/associations/required_test.rb index 8b765a2e0c..3e5494e897 100644 --- a/activerecord/test/cases/associations/required_test.rb +++ b/activerecord/test/cases/associations/required_test.rb @@ -1,7 +1,7 @@ require "cases/helper" class RequiredAssociationsTest < ActiveRecord::TestCase - self.use_transactional_fixtures = false + self.use_transactional_tests = false class Parent < ActiveRecord::Base end diff --git a/activerecord/test/cases/autosave_association_test.rb b/activerecord/test/cases/autosave_association_test.rb index 859afc4553..80b5a0004d 100644 --- a/activerecord/test/cases/autosave_association_test.rb +++ b/activerecord/test/cases/autosave_association_test.rb @@ -43,7 +43,7 @@ class TestAutosaveAssociationsInGeneral < ActiveRecord::TestCase reference = Class.new(ActiveRecord::Base) { self.table_name = "references" def self.name; 'Reference'; end - belongs_to :person, autosave: true, class: person + belongs_to :person, autosave: true, anonymous_class: person } u = person.create!(first_name: 'cool') @@ -629,7 +629,7 @@ class TestDefaultAutosaveAssociationOnNewRecord < ActiveRecord::TestCase end class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase - self.use_transactional_fixtures = false + self.use_transactional_tests = false setup do @pirate = Pirate.create(:catchphrase => "Don' botharrr talkin' like one, savvy?") @@ -637,7 +637,7 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase end teardown do - # We are running without transactional fixtures and need to cleanup. + # We are running without transactional tests and need to cleanup. Bird.delete_all Parrot.delete_all @ship.delete @@ -1009,7 +1009,7 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase end class TestAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCase - self.use_transactional_fixtures = false unless supports_savepoints? + self.use_transactional_tests = false unless supports_savepoints? def setup super @@ -1145,7 +1145,7 @@ class TestAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCase end class TestAutosaveAssociationOnAHasOneThroughAssociation < ActiveRecord::TestCase - self.use_transactional_fixtures = false unless supports_savepoints? + self.use_transactional_tests = false unless supports_savepoints? def setup super @@ -1166,7 +1166,7 @@ class TestAutosaveAssociationOnAHasOneThroughAssociation < ActiveRecord::TestCas end class TestAutosaveAssociationOnABelongsToAssociation < ActiveRecord::TestCase - self.use_transactional_fixtures = false unless supports_savepoints? + self.use_transactional_tests = false unless supports_savepoints? def setup super @@ -1278,6 +1278,16 @@ module AutosaveAssociationOnACollectionAssociationTests assert_equal new_names, @pirate.reload.send(@association_name).map(&:name) end + def test_should_update_children_when_autosave_is_true_and_parent_is_new_but_child_is_not + parrot = Parrot.create!(name: "Polly") + parrot.name = "Squawky" + pirate = Pirate.new(parrots: [parrot], catchphrase: "Arrrr") + + pirate.save! + + assert_equal "Squawky", parrot.reload.name + end + def test_should_automatically_validate_the_associated_models @pirate.send(@association_name).each { |child| child.name = '' } @@ -1414,7 +1424,7 @@ module AutosaveAssociationOnACollectionAssociationTests end class TestAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCase - self.use_transactional_fixtures = false unless supports_savepoints? + self.use_transactional_tests = false unless supports_savepoints? def setup super @@ -1430,7 +1440,7 @@ class TestAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCase end class TestAutosaveAssociationOnAHasAndBelongsToManyAssociation < ActiveRecord::TestCase - self.use_transactional_fixtures = false unless supports_savepoints? + self.use_transactional_tests = false unless supports_savepoints? def setup super @@ -1447,7 +1457,7 @@ class TestAutosaveAssociationOnAHasAndBelongsToManyAssociation < ActiveRecord::T end class TestAutosaveAssociationOnAHasAndBelongsToManyAssociationWithAcceptsNestedAttributes < ActiveRecord::TestCase - self.use_transactional_fixtures = false unless supports_savepoints? + self.use_transactional_tests = false unless supports_savepoints? def setup super @@ -1464,7 +1474,7 @@ class TestAutosaveAssociationOnAHasAndBelongsToManyAssociationWithAcceptsNestedA end class TestAutosaveAssociationValidationsOnAHasManyAssociation < ActiveRecord::TestCase - self.use_transactional_fixtures = false unless supports_savepoints? + self.use_transactional_tests = false unless supports_savepoints? def setup super @@ -1481,7 +1491,7 @@ class TestAutosaveAssociationValidationsOnAHasManyAssociation < ActiveRecord::Te end class TestAutosaveAssociationValidationsOnAHasOneAssociation < ActiveRecord::TestCase - self.use_transactional_fixtures = false unless supports_savepoints? + self.use_transactional_tests = false unless supports_savepoints? def setup super @@ -1504,7 +1514,7 @@ class TestAutosaveAssociationValidationsOnAHasOneAssociation < ActiveRecord::Tes end class TestAutosaveAssociationValidationsOnABelongsToAssociation < ActiveRecord::TestCase - self.use_transactional_fixtures = false unless supports_savepoints? + self.use_transactional_tests = false unless supports_savepoints? def setup super @@ -1525,7 +1535,7 @@ class TestAutosaveAssociationValidationsOnABelongsToAssociation < ActiveRecord:: end class TestAutosaveAssociationValidationsOnAHABTMAssociation < ActiveRecord::TestCase - self.use_transactional_fixtures = false unless supports_savepoints? + self.use_transactional_tests = false unless supports_savepoints? def setup super @@ -1548,7 +1558,7 @@ class TestAutosaveAssociationValidationsOnAHABTMAssociation < ActiveRecord::Test end class TestAutosaveAssociationValidationMethodsGeneration < ActiveRecord::TestCase - self.use_transactional_fixtures = false unless supports_savepoints? + self.use_transactional_tests = false unless supports_savepoints? def setup super diff --git a/activerecord/test/cases/batches_test.rb b/activerecord/test/cases/batches_test.rb index 9e428098e4..0791dde1f2 100644 --- a/activerecord/test/cases/batches_test.rb +++ b/activerecord/test/cases/batches_test.rb @@ -190,8 +190,9 @@ class EachTest < ActiveRecord::TestCase def test_find_in_batches_should_use_any_column_as_primary_key_when_start_is_not_specified assert_queries(Subscriber.count + 1) do - Subscriber.find_each(:batch_size => 1) do |subscriber| - assert_kind_of Subscriber, subscriber + Subscriber.find_in_batches(batch_size: 1) do |batch| + assert_kind_of Array, batch + assert_kind_of Subscriber, batch.first end end end diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb index f0393aa6b1..8fc996ee74 100644 --- a/activerecord/test/cases/calculations_test.rb +++ b/activerecord/test/cases/calculations_test.rb @@ -11,6 +11,10 @@ require 'models/minivan' require 'models/speedometer' require 'models/ship_part' require 'models/treasure' +require 'models/developer' +require 'models/comment' +require 'models/rating' +require 'models/post' class NumericData < ActiveRecord::Base self.table_name = 'numeric_data' @@ -636,4 +640,11 @@ class CalculationsTest < ActiveRecord::TestCase Client.update_all(client_of: nil) assert_equal({ nil => Client.count }, Client.group(:firm).count) end + + def test_should_reference_correct_aliases_while_joining_tables_of_has_many_through_association + assert_nothing_raised ActiveRecord::StatementInvalid do + developer = Developer.create!(name: 'developer') + developer.ratings.includes(comment: :post).where(posts: { id: 1 }).count + end + end end diff --git a/activerecord/test/cases/connection_management_test.rb b/activerecord/test/cases/connection_management_test.rb index f53c496ecd..bab624b78a 100644 --- a/activerecord/test/cases/connection_management_test.rb +++ b/activerecord/test/cases/connection_management_test.rb @@ -110,7 +110,7 @@ module ActiveRecord assert ActiveRecord::Base.connection_handler.active_connections? end - test "proxy is polite to it's body and responds to it" do + test "proxy is polite to its body and responds to it" do body = Class.new(String) { def to_path; "/path"; end }.new app = lambda { |_| [200, {}, body] } response_body = ConnectionManagement.new(app).call(@env)[2] diff --git a/activerecord/test/cases/connection_pool_test.rb b/activerecord/test/cases/connection_pool_test.rb index 8d15a76735..aa50efc979 100644 --- a/activerecord/test/cases/connection_pool_test.rb +++ b/activerecord/test/cases/connection_pool_test.rb @@ -204,13 +204,13 @@ module ActiveRecord end # The connection pool is "fair" if threads waiting for - # connections receive them the order in which they began + # connections receive them in the order in which they began # waiting. This ensures that we don't timeout one HTTP request # even while well under capacity in a multi-threaded environment # such as a Java servlet container. # # We don't need strict fairness: if two connections become - # available at the same time, it's fine of two threads that were + # available at the same time, it's fine if two threads that were # waiting acquire the connections out of order. # # Thus this test prepares waiting threads and then trickles in diff --git a/activerecord/test/cases/date_time_precision_test.rb b/activerecord/test/cases/date_time_precision_test.rb index 6a4e64b22c..698f1b852e 100644 --- a/activerecord/test/cases/date_time_precision_test.rb +++ b/activerecord/test/cases/date_time_precision_test.rb @@ -4,7 +4,7 @@ require 'support/schema_dumping_helper' if ActiveRecord::Base.connection.supports_datetime_with_precision? class DateTimePrecisionTest < ActiveRecord::TestCase include SchemaDumpingHelper - self.use_transactional_fixtures = false + self.use_transactional_tests = false class Foo < ActiveRecord::Base; end diff --git a/activerecord/test/cases/defaults_test.rb b/activerecord/test/cases/defaults_test.rb index b9db0d0123..67fddebf45 100644 --- a/activerecord/test/cases/defaults_test.rb +++ b/activerecord/test/cases/defaults_test.rb @@ -90,14 +90,14 @@ end if current_adapter?(:MysqlAdapter, :Mysql2Adapter) class DefaultsTestWithoutTransactionalFixtures < ActiveRecord::TestCase # ActiveRecord::Base#create! (and #save and other related methods) will - # open a new transaction. When in transactional fixtures mode, this will + # open a new transaction. When in transactional tests mode, this will # cause Active Record to create a new savepoint. However, since MySQL doesn't # support DDL transactions, creating a table will result in any created # savepoints to be automatically released. This in turn causes the savepoint # release code in AbstractAdapter#transaction to fail. # - # We don't want that to happen, so we disable transactional fixtures here. - self.use_transactional_fixtures = false + # We don't want that to happen, so we disable transactional tests here. + self.use_transactional_tests = false def using_strict(strict) connection = ActiveRecord::Base.remove_connection diff --git a/activerecord/test/cases/disconnected_test.rb b/activerecord/test/cases/disconnected_test.rb index 94447addc1..c25089a420 100644 --- a/activerecord/test/cases/disconnected_test.rb +++ b/activerecord/test/cases/disconnected_test.rb @@ -4,7 +4,7 @@ class TestRecord < ActiveRecord::Base end class TestDisconnectedAdapter < ActiveRecord::TestCase - self.use_transactional_fixtures = false + self.use_transactional_tests = false def setup @connection = ActiveRecord::Base.connection @@ -21,7 +21,9 @@ class TestDisconnectedAdapter < ActiveRecord::TestCase @connection.execute "SELECT count(*) from products" @connection.disconnect! assert_raises(ActiveRecord::StatementInvalid) do - @connection.execute "SELECT count(*) from products" + silence_warnings do + @connection.execute "SELECT count(*) from products" + end end end end diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb index 39308866ee..4b819a82e8 100644 --- a/activerecord/test/cases/finder_test.rb +++ b/activerecord/test/cases/finder_test.rb @@ -947,7 +947,6 @@ class FinderTest < ActiveRecord::TestCase end end - # http://dev.rubyonrails.org/ticket/6778 def test_find_ignores_previously_inserted_record Post.create!(:title => 'test', :body => 'it out') assert_equal [], Post.where(id: nil) diff --git a/activerecord/test/cases/fixtures_test.rb b/activerecord/test/cases/fixtures_test.rb index d1add21219..97ba178b4d 100644 --- a/activerecord/test/cases/fixtures_test.rb +++ b/activerecord/test/cases/fixtures_test.rb @@ -28,7 +28,7 @@ require 'tempfile' class FixturesTest < ActiveRecord::TestCase self.use_instantiated_fixtures = true - self.use_transactional_fixtures = false + self.use_transactional_tests = false # other_topics fixture should not be included here fixtures :topics, :developers, :accounts, :tasks, :categories, :funny_jokes, :binaries, :traffic_lights @@ -279,10 +279,10 @@ class HasManyThroughFixture < ActiveSupport::TestCase treasure = make_model "Treasure" pt.table_name = "parrots_treasures" - pt.belongs_to :parrot, :class => parrot - pt.belongs_to :treasure, :class => treasure + pt.belongs_to :parrot, :anonymous_class => parrot + pt.belongs_to :treasure, :anonymous_class => treasure - parrot.has_many :parrot_treasures, :class => pt + parrot.has_many :parrot_treasures, :anonymous_class => pt parrot.has_many :treasures, :through => :parrot_treasures parrots = File.join FIXTURES_ROOT, 'parrots' @@ -297,10 +297,10 @@ class HasManyThroughFixture < ActiveSupport::TestCase parrot = make_model "Parrot" treasure = make_model "Treasure" - pt.belongs_to :parrot, :class => parrot - pt.belongs_to :treasure, :class => treasure + pt.belongs_to :parrot, :anonymous_class => parrot + pt.belongs_to :treasure, :anonymous_class => treasure - parrot.has_many :parrot_treasures, :class => pt + parrot.has_many :parrot_treasures, :anonymous_class => pt parrot.has_many :treasures, :through => :parrot_treasures parrots = File.join FIXTURES_ROOT, 'parrots' @@ -419,7 +419,7 @@ end class TransactionalFixturesTest < ActiveRecord::TestCase self.use_instantiated_fixtures = true - self.use_transactional_fixtures = true + self.use_transactional_tests = true fixtures :topics @@ -511,7 +511,7 @@ class CheckSetTableNameFixturesTest < ActiveRecord::TestCase fixtures :funny_jokes # Set to false to blow away fixtures cache and ensure our fixtures are loaded # and thus takes into account our set_fixture_class - self.use_transactional_fixtures = false + self.use_transactional_tests = false def test_table_method assert_kind_of Joke, funny_jokes(:a_joke) @@ -523,7 +523,7 @@ class FixtureNameIsNotTableNameFixturesTest < ActiveRecord::TestCase fixtures :items # Set to false to blow away fixtures cache and ensure our fixtures are loaded # and thus takes into account our set_fixture_class - self.use_transactional_fixtures = false + self.use_transactional_tests = false def test_named_accessor assert_kind_of Book, items(:dvd) @@ -535,7 +535,7 @@ class FixtureNameIsNotTableNameMultipleFixturesTest < ActiveRecord::TestCase fixtures :items, :funny_jokes # Set to false to blow away fixtures cache and ensure our fixtures are loaded # and thus takes into account our set_fixture_class - self.use_transactional_fixtures = false + self.use_transactional_tests = false def test_named_accessor_of_differently_named_fixture assert_kind_of Book, items(:dvd) @@ -549,7 +549,7 @@ end class CustomConnectionFixturesTest < ActiveRecord::TestCase set_fixture_class :courses => Course fixtures :courses - self.use_transactional_fixtures = false + self.use_transactional_tests = false def test_leaky_destroy assert_nothing_raised { courses(:ruby) } @@ -564,7 +564,7 @@ end class TransactionalFixturesOnCustomConnectionTest < ActiveRecord::TestCase set_fixture_class :courses => Course fixtures :courses - self.use_transactional_fixtures = true + self.use_transactional_tests = true def test_leaky_destroy assert_nothing_raised { courses(:ruby) } @@ -580,7 +580,7 @@ class InvalidTableNameFixturesTest < ActiveRecord::TestCase fixtures :funny_jokes # Set to false to blow away fixtures cache and ensure our fixtures are loaded # and thus takes into account our lack of set_fixture_class - self.use_transactional_fixtures = false + self.use_transactional_tests = false def test_raises_error assert_raise ActiveRecord::FixtureClassNotFound do @@ -594,7 +594,7 @@ class CheckEscapedYamlFixturesTest < ActiveRecord::TestCase fixtures :funny_jokes # Set to false to blow away fixtures cache and ensure our fixtures are loaded # and thus takes into account our set_fixture_class - self.use_transactional_fixtures = false + self.use_transactional_tests = false def test_proper_escaped_fixture assert_equal "The \\n Aristocrats\nAte the candy\n", funny_jokes(:another_joke).name @@ -664,7 +664,7 @@ class LoadAllFixturesWithPathnameTest < ActiveRecord::TestCase end class FasterFixturesTest < ActiveRecord::TestCase - self.use_transactional_fixtures = false + self.use_transactional_tests = false fixtures :categories, :authors def load_extra_fixture(name) diff --git a/activerecord/test/cases/helper.rb b/activerecord/test/cases/helper.rb index b118ecc125..12c793c408 100644 --- a/activerecord/test/cases/helper.rb +++ b/activerecord/test/cases/helper.rb @@ -143,7 +143,7 @@ class ActiveSupport::TestCase self.fixture_path = FIXTURES_ROOT self.use_instantiated_fixtures = false - self.use_transactional_fixtures = true + self.use_transactional_tests = true def create_fixtures(*fixture_set_names, &block) ActiveRecord::FixtureSet.create_fixtures(ActiveSupport::TestCase.fixture_path, fixture_set_names, fixture_class_names, &block) diff --git a/activerecord/test/cases/hot_compatibility_test.rb b/activerecord/test/cases/hot_compatibility_test.rb index b4617cf6f9..5ba9a1029a 100644 --- a/activerecord/test/cases/hot_compatibility_test.rb +++ b/activerecord/test/cases/hot_compatibility_test.rb @@ -1,7 +1,7 @@ require 'cases/helper' class HotCompatibilityTest < ActiveRecord::TestCase - self.use_transactional_fixtures = false + self.use_transactional_tests = false setup do @klass = Class.new(ActiveRecord::Base) do diff --git a/activerecord/test/cases/invalid_connection_test.rb b/activerecord/test/cases/invalid_connection_test.rb index 8416c81f45..6523fc29fd 100644 --- a/activerecord/test/cases/invalid_connection_test.rb +++ b/activerecord/test/cases/invalid_connection_test.rb @@ -1,7 +1,7 @@ require "cases/helper" class TestAdapterWithInvalidConnection < ActiveRecord::TestCase - self.use_transactional_fixtures = false + self.use_transactional_tests = false class Bird < ActiveRecord::Base end diff --git a/activerecord/test/cases/locking_test.rb b/activerecord/test/cases/locking_test.rb index 848174df06..dbdcc84b7d 100644 --- a/activerecord/test/cases/locking_test.rb +++ b/activerecord/test/cases/locking_test.rb @@ -177,6 +177,16 @@ class OptimisticLockingTest < ActiveRecord::TestCase assert_equal 1, p1.lock_version end + def test_touch_stale_object + person = Person.create!(first_name: 'Mehmet Emin') + stale_person = Person.find(person.id) + person.update_attribute(:gender, 'M') + + assert_raises(ActiveRecord::StaleObjectError) do + stale_person.touch + end + end + def test_lock_column_name_existing t1 = LegacyThing.find(1) t2 = LegacyThing.find(1) @@ -285,10 +295,10 @@ end class OptimisticLockingWithSchemaChangeTest < ActiveRecord::TestCase fixtures :people, :legacy_things, :references - # need to disable transactional fixtures, because otherwise the sqlite3 + # need to disable transactional tests, because otherwise the sqlite3 # adapter (at least) chokes when we try and change the schema in the middle # of a test (see test_increment_counter_*). - self.use_transactional_fixtures = false + self.use_transactional_tests = false { :lock_version => Person, :custom_lock_version => LegacyThing }.each do |name, model| define_method("test_increment_counter_updates_#{name}") do @@ -365,7 +375,7 @@ end # (See exec vs. async_exec in the PostgreSQL adapter.) unless in_memory_db? class PessimisticLockingTest < ActiveRecord::TestCase - self.use_transactional_fixtures = false + self.use_transactional_tests = false fixtures :people, :readers def setup diff --git a/activerecord/test/cases/migration/change_schema_test.rb b/activerecord/test/cases/migration/change_schema_test.rb index 30c91dfdcb..46a62c272f 100644 --- a/activerecord/test/cases/migration/change_schema_test.rb +++ b/activerecord/test/cases/migration/change_schema_test.rb @@ -426,7 +426,7 @@ module ActiveRecord if ActiveRecord::Base.connection.supports_foreign_keys? class ChangeSchemaWithDependentObjectsTest < ActiveRecord::TestCase - self.use_transactional_fixtures = false + self.use_transactional_tests = false setup do @connection = ActiveRecord::Base.connection diff --git a/activerecord/test/cases/migration/column_attributes_test.rb b/activerecord/test/cases/migration/column_attributes_test.rb index 763aa88f72..8d8e661aa5 100644 --- a/activerecord/test/cases/migration/column_attributes_test.rb +++ b/activerecord/test/cases/migration/column_attributes_test.rb @@ -5,7 +5,7 @@ module ActiveRecord class ColumnAttributesTest < ActiveRecord::TestCase include ActiveRecord::Migration::TestHelper - self.use_transactional_fixtures = false + self.use_transactional_tests = false def test_add_column_newline_default string = "foo\nbar" diff --git a/activerecord/test/cases/migration/columns_test.rb b/activerecord/test/cases/migration/columns_test.rb index e5ccfe0f91..5fc7702dfa 100644 --- a/activerecord/test/cases/migration/columns_test.rb +++ b/activerecord/test/cases/migration/columns_test.rb @@ -5,7 +5,7 @@ module ActiveRecord class ColumnsTest < ActiveRecord::TestCase include ActiveRecord::Migration::TestHelper - self.use_transactional_fixtures = false + self.use_transactional_tests = false # FIXME: this is more of an integration test with AR::Base and the # schema modifications. Maybe we should move this? diff --git a/activerecord/test/cases/migration/logger_test.rb b/activerecord/test/cases/migration/logger_test.rb index 319d3e1af3..bf6e684887 100644 --- a/activerecord/test/cases/migration/logger_test.rb +++ b/activerecord/test/cases/migration/logger_test.rb @@ -4,7 +4,7 @@ module ActiveRecord class Migration class LoggerTest < ActiveRecord::TestCase # MySQL can't roll back ddl changes - self.use_transactional_fixtures = false + self.use_transactional_tests = false Migration = Struct.new(:name, :version) do def disable_ddl_transaction; false end diff --git a/activerecord/test/cases/migration/references_foreign_key_test.rb b/activerecord/test/cases/migration/references_foreign_key_test.rb index 17ac72a109..87348d0f90 100644 --- a/activerecord/test/cases/migration/references_foreign_key_test.rb +++ b/activerecord/test/cases/migration/references_foreign_key_test.rb @@ -105,6 +105,28 @@ module ActiveRecord @connection.remove_reference :testings, :testing_parent, foreign_key: true end end + + test "foreign key methods respect pluralize_table_names" do + begin + original_pluralize_table_names = ActiveRecord::Base.pluralize_table_names + ActiveRecord::Base.pluralize_table_names = false + @connection.create_table :testing + @connection.change_table :testing_parents do |t| + t.references :testing, foreign_key: true + end + + fk = @connection.foreign_keys("testing_parents").first + assert_equal "testing_parents", fk.from_table + assert_equal "testing", fk.to_table + + assert_difference "@connection.foreign_keys('testing_parents').size", -1 do + @connection.remove_reference :testing_parents, :testing, foreign_key: true + end + ensure + ActiveRecord::Base.pluralize_table_names = original_pluralize_table_names + @connection.drop_table "testing", if_exists: true + end + end end end end diff --git a/activerecord/test/cases/migration/references_statements_test.rb b/activerecord/test/cases/migration/references_statements_test.rb index 988bd9c89f..f613fd66c3 100644 --- a/activerecord/test/cases/migration/references_statements_test.rb +++ b/activerecord/test/cases/migration/references_statements_test.rb @@ -5,7 +5,7 @@ module ActiveRecord class ReferencesStatementsTest < ActiveRecord::TestCase include ActiveRecord::Migration::TestHelper - self.use_transactional_fixtures = false + self.use_transactional_tests = false def setup super diff --git a/activerecord/test/cases/migration/rename_table_test.rb b/activerecord/test/cases/migration/rename_table_test.rb index 3eef308428..6d742d3f2f 100644 --- a/activerecord/test/cases/migration/rename_table_test.rb +++ b/activerecord/test/cases/migration/rename_table_test.rb @@ -5,7 +5,7 @@ module ActiveRecord class RenameTableTest < ActiveRecord::TestCase include ActiveRecord::Migration::TestHelper - self.use_transactional_fixtures = false + self.use_transactional_tests = false def setup super diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb index 3b73685a2c..b2f209fe97 100644 --- a/activerecord/test/cases/migration_test.rb +++ b/activerecord/test/cases/migration_test.rb @@ -24,7 +24,7 @@ class Reminder < ActiveRecord::Base; end class Thing < ActiveRecord::Base; end class MigrationTest < ActiveRecord::TestCase - self.use_transactional_fixtures = false + self.use_transactional_tests = false fixtures :people diff --git a/activerecord/test/cases/migrator_test.rb b/activerecord/test/cases/migrator_test.rb index 1760314099..2ff6938e7b 100644 --- a/activerecord/test/cases/migrator_test.rb +++ b/activerecord/test/cases/migrator_test.rb @@ -2,7 +2,7 @@ require "cases/helper" require "cases/migration/helper" class MigratorTest < ActiveRecord::TestCase - self.use_transactional_fixtures = false + self.use_transactional_tests = false # Use this class to sense if migrations have gone # up or down. diff --git a/activerecord/test/cases/modules_test.rb b/activerecord/test/cases/modules_test.rb index 6f65bf80eb..7f31325f47 100644 --- a/activerecord/test/cases/modules_test.rb +++ b/activerecord/test/cases/modules_test.rb @@ -68,8 +68,7 @@ class ModulesTest < ActiveRecord::TestCase end end - # need to add an eager loading condition to force the eager loading model into - # the old join model, to test that. See http://dev.rubyonrails.org/ticket/9640 + # An eager loading condition to force the eager loading model into the old join model. def test_eager_loading_in_modules clients = [] diff --git a/activerecord/test/cases/multiple_db_test.rb b/activerecord/test/cases/multiple_db_test.rb index f9bc266e84..39cdcf5403 100644 --- a/activerecord/test/cases/multiple_db_test.rb +++ b/activerecord/test/cases/multiple_db_test.rb @@ -4,7 +4,7 @@ require 'models/bird' require 'models/course' class MultipleDbTest < ActiveRecord::TestCase - self.use_transactional_fixtures = false + self.use_transactional_tests = false def setup @courses = create_fixtures("courses") { Course.retrieve_connection } diff --git a/activerecord/test/cases/nested_attributes_test.rb b/activerecord/test/cases/nested_attributes_test.rb index c5f6589c22..6b4addd52f 100644 --- a/activerecord/test/cases/nested_attributes_test.rb +++ b/activerecord/test/cases/nested_attributes_test.rb @@ -943,7 +943,7 @@ class TestNestedAttributesWithNonStandardPrimaryKeys < ActiveRecord::TestCase end class TestHasOneAutosaveAssociationWhichItselfHasAutosaveAssociations < ActiveRecord::TestCase - self.use_transactional_fixtures = false unless supports_savepoints? + self.use_transactional_tests = false unless supports_savepoints? def setup @pirate = Pirate.create!(:catchphrase => "My baby takes tha mornin' train!") @@ -983,7 +983,7 @@ class TestHasOneAutosaveAssociationWhichItselfHasAutosaveAssociations < ActiveRe end class TestHasManyAutosaveAssociationWhichItselfHasAutosaveAssociations < ActiveRecord::TestCase - self.use_transactional_fixtures = false unless supports_savepoints? + self.use_transactional_tests = false unless supports_savepoints? def setup @ship = Ship.create!(:name => "The good ship Dollypop") diff --git a/activerecord/test/cases/persistence_test.rb b/activerecord/test/cases/persistence_test.rb index 7c281a95b7..1e93e2a05c 100644 --- a/activerecord/test/cases/persistence_test.rb +++ b/activerecord/test/cases/persistence_test.rb @@ -898,7 +898,7 @@ class PersistenceTest < ActiveRecord::TestCase end class SaveTest < ActiveRecord::TestCase - self.use_transactional_fixtures = false + self.use_transactional_tests = false def test_save_touch_false widget = Class.new(ActiveRecord::Base) do @@ -924,7 +924,8 @@ class PersistenceTest < ActiveRecord::TestCase assert_equal instance.created_at, created_at assert_equal instance.updated_at, updated_at ensure - ActiveRecord::Base.connection.drop_table :widgets + ActiveRecord::Base.connection.drop_table widget.table_name + widget.reset_column_information end end end diff --git a/activerecord/test/cases/pooled_connections_test.rb b/activerecord/test/cases/pooled_connections_test.rb index 287a3f33ea..daa3271777 100644 --- a/activerecord/test/cases/pooled_connections_test.rb +++ b/activerecord/test/cases/pooled_connections_test.rb @@ -3,7 +3,7 @@ require "models/project" require "timeout" class PooledConnectionsTest < ActiveRecord::TestCase - self.use_transactional_fixtures = false + self.use_transactional_tests = false def setup @per_test_teardown = [] diff --git a/activerecord/test/cases/primary_keys_test.rb b/activerecord/test/cases/primary_keys_test.rb index 1ea1ef5e12..83be9a75d8 100644 --- a/activerecord/test/cases/primary_keys_test.rb +++ b/activerecord/test/cases/primary_keys_test.rb @@ -178,7 +178,7 @@ class PrimaryKeysTest < ActiveRecord::TestCase end class PrimaryKeyWithNoConnectionTest < ActiveRecord::TestCase - self.use_transactional_fixtures = false + self.use_transactional_tests = false unless in_memory_db? def test_set_primary_key_with_no_connection @@ -199,7 +199,7 @@ end class PrimaryKeyAnyTypeTest < ActiveRecord::TestCase include SchemaDumpingHelper - self.use_transactional_fixtures = false + self.use_transactional_tests = false class Barcode < ActiveRecord::Base end @@ -229,7 +229,7 @@ end if current_adapter?(:MysqlAdapter, :Mysql2Adapter) class PrimaryKeyWithAnsiQuotesTest < ActiveRecord::TestCase - self.use_transactional_fixtures = false + self.use_transactional_tests = false def test_primary_key_method_with_ansi_quotes con = ActiveRecord::Base.connection @@ -245,7 +245,7 @@ if current_adapter?(:PostgreSQLAdapter, :MysqlAdapter, :Mysql2Adapter) class PrimaryKeyBigSerialTest < ActiveRecord::TestCase include SchemaDumpingHelper - self.use_transactional_fixtures = false + self.use_transactional_tests = false class Widget < ActiveRecord::Base end @@ -260,7 +260,8 @@ if current_adapter?(:PostgreSQLAdapter, :MysqlAdapter, :Mysql2Adapter) end teardown do - @connection.drop_table 'widgets', if_exists: true + @connection.drop_table :widgets, if_exists: true + Widget.reset_column_information end test "primary key column type with bigserial" do diff --git a/activerecord/test/cases/query_cache_test.rb b/activerecord/test/cases/query_cache_test.rb index 744f9edc47..2f0b5df286 100644 --- a/activerecord/test/cases/query_cache_test.rb +++ b/activerecord/test/cases/query_cache_test.rb @@ -184,7 +184,7 @@ class QueryCacheTest < ActiveRecord::TestCase # Oracle adapter returns count() as Fixnum or Float if current_adapter?(:OracleAdapter) assert_kind_of Numeric, Task.connection.select_value("SELECT count(*) AS count_all FROM tasks") - elsif current_adapter?(:SQLite3Adapter, :Mysql2Adapter) + elsif current_adapter?(:SQLite3Adapter, :Mysql2Adapter, :PostgreSQLAdapter) # Future versions of the sqlite3 adapter will return numeric assert_instance_of Fixnum, Task.connection.select_value("SELECT count(*) AS count_all FROM tasks") diff --git a/activerecord/test/cases/reflection_test.rb b/activerecord/test/cases/reflection_test.rb index 67e9bef808..7b47c80331 100644 --- a/activerecord/test/cases/reflection_test.rb +++ b/activerecord/test/cases/reflection_test.rb @@ -23,6 +23,7 @@ require 'models/chef' require 'models/department' require 'models/cake_designer' require 'models/drink_designer' +require 'models/recipe' class ReflectionTest < ActiveRecord::TestCase include ActiveRecord::Reflection @@ -277,6 +278,22 @@ class ReflectionTest < ActiveRecord::TestCase assert_equal 2, @hotel.chefs.size end + def test_scope_chain_of_polymorphic_association_does_not_leak_into_other_hmt_associations + hotel = Hotel.create! + department = hotel.departments.create! + drink = department.chefs.create!(employable: DrinkDesigner.create!) + Recipe.create!(chef_id: drink.id, hotel_id: hotel.id) + + expected_sql = capture_sql { hotel.recipes.to_a } + + Hotel.reflect_on_association(:recipes).clear_association_scope_cache + hotel.reload + hotel.drink_designers.to_a + loaded_sql = capture_sql { hotel.recipes.to_a } + + assert_equal expected_sql, loaded_sql + end + def test_nested? assert !Author.reflect_on_association(:comments).nested? assert Author.reflect_on_association(:tags).nested? diff --git a/activerecord/test/cases/relation/record_fetch_warning_test.rb b/activerecord/test/cases/relation/record_fetch_warning_test.rb new file mode 100644 index 0000000000..62f0a7cc49 --- /dev/null +++ b/activerecord/test/cases/relation/record_fetch_warning_test.rb @@ -0,0 +1,28 @@ +require 'cases/helper' +require 'models/post' + +module ActiveRecord + class RecordFetchWarningTest < ActiveRecord::TestCase + fixtures :posts + + def test_warn_on_records_fetched_greater_than + original_logger = ActiveRecord::Base.logger + orginal_warn_on_records_fetched_greater_than = ActiveRecord::Base.warn_on_records_fetched_greater_than + + log = StringIO.new + ActiveRecord::Base.logger = ActiveSupport::Logger.new(log) + ActiveRecord::Base.logger.level = Logger::WARN + + require 'active_record/relation/record_fetch_warning' + + ActiveRecord::Base.warn_on_records_fetched_greater_than = 1 + + Post.all.to_a + + assert_match(/Query fetched/, log.string) + ensure + ActiveRecord::Base.logger = original_logger + ActiveRecord::Base.warn_on_records_fetched_greater_than = orginal_warn_on_records_fetched_greater_than + end + end +end diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb index 513f65f707..63612e33af 100644 --- a/activerecord/test/cases/schema_dumper_test.rb +++ b/activerecord/test/cases/schema_dumper_test.rb @@ -3,7 +3,7 @@ require 'support/schema_dumping_helper' class SchemaDumperTest < ActiveRecord::TestCase include SchemaDumpingHelper - self.use_transactional_fixtures = false + self.use_transactional_tests = false setup do ActiveRecord::SchemaMigration.create_table @@ -73,7 +73,7 @@ class SchemaDumperTest < ActiveRecord::TestCase next if column_set.empty? lengths = column_set.map do |column| - if match = column.match(/t\.(?:integer|decimal|float|datetime|timestamp|time|date|text|binary|string|boolean|xml|uuid|point)\s+"/) + if match = column.match(/\bt\.\w+\s+"/) match[0].length end end.compact @@ -248,6 +248,11 @@ class SchemaDumperTest < ActiveRecord::TestCase assert_match %r{t\.integer\s+"bigint_default",\s+limit: 8,\s+default: 0}, output end + def test_schema_dump_includes_limit_on_array_type + output = standard_dump + assert_match %r{t\.integer\s+"big_int_data_points\",\s+limit: 8,\s+array: true}, output + end + if ActiveRecord::Base.connection.supports_extensions? def test_schema_dump_includes_extensions connection = ActiveRecord::Base.connection diff --git a/activerecord/test/cases/serialized_attribute_test.rb b/activerecord/test/cases/serialized_attribute_test.rb index e29f7462c8..7c92453ee3 100644 --- a/activerecord/test/cases/serialized_attribute_test.rb +++ b/activerecord/test/cases/serialized_attribute_test.rb @@ -264,4 +264,14 @@ class SerializedAttributeTest < ActiveRecord::TestCase Topic.serialize(:content, Regexp) end end + + def test_newly_emptied_serialized_hash_is_changed + Topic.serialize(:content, Hash) + topic = Topic.create(content: { "things" => "stuff" }) + topic.content.delete("things") + topic.save! + topic.reload + + assert_equal({}, topic.content) + end end diff --git a/activerecord/test/cases/tasks/postgresql_rake_test.rb b/activerecord/test/cases/tasks/postgresql_rake_test.rb index 0d574d071c..084302cde5 100644 --- a/activerecord/test/cases/tasks/postgresql_rake_test.rb +++ b/activerecord/test/cases/tasks/postgresql_rake_test.rb @@ -195,21 +195,54 @@ module ActiveRecord 'adapter' => 'postgresql', 'database' => 'my-app-db' } + @filename = "awesome-file.sql" ActiveRecord::Base.stubs(:connection).returns(@connection) ActiveRecord::Base.stubs(:establish_connection).returns(true) Kernel.stubs(:system) + File.stubs(:open) end def test_structure_dump - filename = "awesome-file.sql" - Kernel.expects(:system).with("pg_dump -i -s -x -O -f #{filename} my-app-db").returns(true) - @connection.expects(:schema_search_path).returns("foo") + Kernel.expects(:system).with("pg_dump -i -s -x -O -f #{@filename} my-app-db").returns(true) + + ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, @filename) + end + + def test_structure_dump_with_schema_search_path + @configuration['schema_search_path'] = 'foo,bar' + + Kernel.expects(:system).with("pg_dump -i -s -x -O -f #{@filename} --schema=foo --schema=bar my-app-db").returns(true) + + ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, @filename) + end + + def test_structure_dump_with_schema_search_path_and_dump_schemas_all + @configuration['schema_search_path'] = 'foo,bar' + + Kernel.expects(:system).with("pg_dump -i -s -x -O -f #{@filename} my-app-db").returns(true) + + with_dump_schemas(:all) do + ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, @filename) + end + end + + def test_structure_dump_with_dump_schemas_string + Kernel.expects(:system).with("pg_dump -i -s -x -O -f #{@filename} --schema=foo --schema=bar my-app-db").returns(true) + + with_dump_schemas('foo,bar') do + ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, @filename) + end + end + + private - ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename) - assert File.exist?(filename) + def with_dump_schemas(value, &block) + old_dump_schemas = ActiveRecord::Base.dump_schemas + ActiveRecord::Base.dump_schemas = value + yield ensure - FileUtils.rm(filename) + ActiveRecord::Base.dump_schemas = old_dump_schemas end end @@ -228,14 +261,14 @@ module ActiveRecord def test_structure_load filename = "awesome-file.sql" - Kernel.expects(:system).with("psql -q -f #{filename} my-app-db") + Kernel.expects(:system).with("psql -X -q -f #{filename} my-app-db") ActiveRecord::Tasks::DatabaseTasks.structure_load(@configuration, filename) end def test_structure_load_accepts_path_with_spaces filename = "awesome file.sql" - Kernel.expects(:system).with("psql -q -f awesome\\ file.sql my-app-db") + Kernel.expects(:system).with("psql -X -q -f awesome\\ file.sql my-app-db") ActiveRecord::Tasks::DatabaseTasks.structure_load(@configuration, filename) end diff --git a/activerecord/test/cases/test_fixtures_test.rb b/activerecord/test/cases/test_fixtures_test.rb new file mode 100644 index 0000000000..3f4baf8378 --- /dev/null +++ b/activerecord/test/cases/test_fixtures_test.rb @@ -0,0 +1,36 @@ +require 'cases/helper' + +class TestFixturesTest < ActiveRecord::TestCase + setup do + @klass = Class.new + @klass.send(:include, ActiveRecord::TestFixtures) + end + + def test_deprecated_use_transactional_fixtures= + assert_deprecated 'use use_transactional_tests= instead' do + @klass.use_transactional_fixtures = true + end + end + + def test_use_transactional_tests_prefers_use_transactional_fixtures + ActiveSupport::Deprecation.silence do + @klass.use_transactional_fixtures = false + end + + assert_equal false, @klass.use_transactional_tests + end + + def test_use_transactional_tests_defaults_to_true + ActiveSupport::Deprecation.silence do + @klass.use_transactional_fixtures = nil + end + + assert_equal true, @klass.use_transactional_tests + end + + def test_use_transactional_tests_can_be_overriden + @klass.use_transactional_tests = "foobar" + + assert_equal "foobar", @klass.use_transactional_tests + end +end diff --git a/activerecord/test/cases/time_precision_test.rb b/activerecord/test/cases/time_precision_test.rb index ff4e5ecec5..ff7a81fe60 100644 --- a/activerecord/test/cases/time_precision_test.rb +++ b/activerecord/test/cases/time_precision_test.rb @@ -4,7 +4,7 @@ require 'support/schema_dumping_helper' if ActiveRecord::Base.connection.supports_datetime_with_precision? class TimePrecisionTest < ActiveRecord::TestCase include SchemaDumpingHelper - self.use_transactional_fixtures = false + self.use_transactional_tests = false class Foo < ActiveRecord::Base; end diff --git a/activerecord/test/cases/timestamp_test.rb b/activerecord/test/cases/timestamp_test.rb index c0c62527df..5dab32995c 100644 --- a/activerecord/test/cases/timestamp_test.rb +++ b/activerecord/test/cases/timestamp_test.rb @@ -446,11 +446,22 @@ class TimestampTest < ActiveRecord::TestCase toy = Toy.first assert_equal [:created_at, :updated_at], toy.send(:all_timestamp_attributes_in_model) end + + def test_index_is_created_for_both_timestamps + ActiveRecord::Base.connection.create_table(:foos, force: true) do |t| + t.timestamps(:foos, null: true, index: true) + end + + indexes = ActiveRecord::Base.connection.indexes('foos') + assert_equal ['created_at', 'updated_at'], indexes.flat_map(&:columns).sort + ensure + ActiveRecord::Base.connection.drop_table(:foos) + end end class TimestampsWithoutTransactionTest < ActiveRecord::TestCase include DdlHelper - self.use_transactional_fixtures = false + self.use_transactional_tests = false class TimestampAttributePost < ActiveRecord::Base attr_accessor :created_at, :updated_at diff --git a/activerecord/test/cases/touch_later_test.rb b/activerecord/test/cases/touch_later_test.rb new file mode 100644 index 0000000000..11804ff90b --- /dev/null +++ b/activerecord/test/cases/touch_later_test.rb @@ -0,0 +1,93 @@ +require 'cases/helper' +require 'models/invoice' +require 'models/line_item' +require 'models/topic' + +class TouchLaterTest < ActiveRecord::TestCase + + def test_touch_laster_raise_if_non_persisted + invoice = Invoice.new + Invoice.transaction do + refute invoice.persisted? + assert_raises(ActiveRecord::ActiveRecordError) do + invoice.touch_later + end + end + end + + def test_touch_later_dont_set_dirty_attributes + invoice = Invoice.create! + invoice.touch_later + refute invoice.changed? + end + + def test_touch_later_update_the_attributes + time = Time.now.utc - 25.days + topic = Topic.create!(updated_at: time, created_at: time) + assert_equal time.to_i, topic.updated_at.to_i + assert_equal time.to_i, topic.created_at.to_i + + Topic.transaction do + topic.touch_later(:created_at) + assert_not_equal time.to_i, topic.updated_at.to_i + assert_not_equal time.to_i, topic.created_at.to_i + + assert_equal time.to_i, topic.reload.updated_at.to_i + assert_equal time.to_i, topic.reload.created_at.to_i + end + assert_not_equal time.to_i, topic.reload.updated_at.to_i + assert_not_equal time.to_i, topic.reload.created_at.to_i + end + + def test_touch_touches_immediately + time = Time.now.utc - 25.days + topic = Topic.create!(updated_at: time, created_at: time) + assert_equal time.to_i, topic.updated_at.to_i + assert_equal time.to_i, topic.created_at.to_i + + Topic.transaction do + topic.touch_later(:created_at) + topic.touch + + assert_not_equal time, topic.reload.updated_at + assert_not_equal time, topic.reload.created_at + end + end + + def test_touch_later_an_association_dont_autosave_parent + time = Time.now.utc - 25.days + line_item = LineItem.create!(amount: 1) + invoice = Invoice.create!(line_items: [line_item]) + invoice.touch(time: time) + + Invoice.transaction do + line_item.update(amount: 2) + assert_equal time.to_i, invoice.reload.updated_at.to_i + end + + assert_not_equal time.to_i, invoice.updated_at.to_i + end + + def test_touch_touches_immediately_with_a_custom_time + time = Time.now.utc - 25.days + topic = Topic.create!(updated_at: time, created_at: time) + assert_equal time, topic.updated_at + assert_equal time, topic.created_at + + Topic.transaction do + topic.touch_later(:created_at) + time = Time.now.utc - 2.days + topic.touch(time: time) + + assert_equal time.to_i, topic.reload.updated_at.to_i + assert_equal time.to_i, topic.reload.created_at.to_i + end + end + + def test_touch_later_dont_hit_the_db + invoice = Invoice.create! + assert_queries(0) do + invoice.touch_later + end + end +end diff --git a/activerecord/test/cases/transaction_callbacks_test.rb b/activerecord/test/cases/transaction_callbacks_test.rb index e868022fed..f2229939c8 100644 --- a/activerecord/test/cases/transaction_callbacks_test.rb +++ b/activerecord/test/cases/transaction_callbacks_test.rb @@ -367,7 +367,7 @@ class TransactionCallbacksTest < ActiveRecord::TestCase end class CallbacksOnMultipleActionsTest < ActiveRecord::TestCase - self.use_transactional_fixtures = false + self.use_transactional_tests = false class TopicWithCallbacksOnMultipleActions < ActiveRecord::Base self.table_name = :topics @@ -376,6 +376,9 @@ class CallbacksOnMultipleActionsTest < ActiveRecord::TestCase after_commit(on: [:create, :update]) { |record| record.history << :create_and_update } after_commit(on: [:update, :destroy]) { |record| record.history << :update_and_destroy } + before_commit(if: :save_before_commit_history) { |record| record.history << :before_commit } + before_commit(if: :update_title) { |record| record.update(title: "before commit title") } + def clear_history @history = [] end @@ -383,6 +386,8 @@ class CallbacksOnMultipleActionsTest < ActiveRecord::TestCase def history @history ||= [] end + + attr_accessor :save_before_commit_history, :update_title end def test_after_commit_on_multiple_actions @@ -399,6 +404,23 @@ class CallbacksOnMultipleActionsTest < ActiveRecord::TestCase topic.destroy assert_equal [:update_and_destroy, :create_and_destroy], topic.history end + + def test_before_commit_actions + topic = TopicWithCallbacksOnMultipleActions.new + topic.save_before_commit_history = true + topic.save + + assert_equal [:before_commit, :create_and_update, :create_and_destroy], topic.history + end + + def test_before_commit_update_in_same_transaction + topic = TopicWithCallbacksOnMultipleActions.new + topic.update_title = true + topic.save + + assert_equal "before commit title", topic.title + assert_equal "before commit title", topic.reload.title + end end diff --git a/activerecord/test/cases/transaction_isolation_test.rb b/activerecord/test/cases/transaction_isolation_test.rb index f89c26532d..2f7d208ed2 100644 --- a/activerecord/test/cases/transaction_isolation_test.rb +++ b/activerecord/test/cases/transaction_isolation_test.rb @@ -2,7 +2,7 @@ require 'cases/helper' unless ActiveRecord::Base.connection.supports_transaction_isolation? class TransactionIsolationUnsupportedTest < ActiveRecord::TestCase - self.use_transactional_fixtures = false + self.use_transactional_tests = false class Tag < ActiveRecord::Base end @@ -17,7 +17,7 @@ end if ActiveRecord::Base.connection.supports_transaction_isolation? class TransactionIsolationTest < ActiveRecord::TestCase - self.use_transactional_fixtures = false + self.use_transactional_tests = false class Tag < ActiveRecord::Base self.table_name = 'tags' diff --git a/activerecord/test/cases/transactions_test.rb b/activerecord/test/cases/transactions_test.rb index 88e595c39f..2468a91969 100644 --- a/activerecord/test/cases/transactions_test.rb +++ b/activerecord/test/cases/transactions_test.rb @@ -9,7 +9,7 @@ require 'models/post' require 'models/movie' class TransactionTest < ActiveRecord::TestCase - self.use_transactional_fixtures = false + self.use_transactional_tests = false fixtures :topics, :developers, :authors, :posts def setup @@ -584,6 +584,24 @@ class TransactionTest < ActiveRecord::TestCase assert_not topic.frozen? end + def test_rollback_of_frozen_records + topic = Topic.create.freeze + Topic.transaction do + topic.destroy + raise ActiveRecord::Rollback + end + assert topic.frozen?, 'frozen' + end + + def test_rollback_for_freshly_persisted_records + topic = Topic.create + Topic.transaction do + topic.destroy + raise ActiveRecord::Rollback + end + assert topic.persisted?, 'persisted' + end + def test_sqlite_add_column_in_transaction return true unless current_adapter?(:SQLite3Adapter) @@ -685,7 +703,7 @@ class TransactionTest < ActiveRecord::TestCase end class TransactionsWithTransactionalFixturesTest < ActiveRecord::TestCase - self.use_transactional_fixtures = true + self.use_transactional_tests = true fixtures :topics def test_automatic_savepoint_in_outer_transaction diff --git a/activerecord/test/cases/unconnected_test.rb b/activerecord/test/cases/unconnected_test.rb index afb893a52c..b210584644 100644 --- a/activerecord/test/cases/unconnected_test.rb +++ b/activerecord/test/cases/unconnected_test.rb @@ -4,7 +4,7 @@ class TestRecord < ActiveRecord::Base end class TestUnconnectedAdapter < ActiveRecord::TestCase - self.use_transactional_fixtures = false + self.use_transactional_tests = false def setup @underlying = ActiveRecord::Base.connection diff --git a/activerecord/test/cases/validations/uniqueness_validation_test.rb b/activerecord/test/cases/validations/uniqueness_validation_test.rb index 062bc733f9..2608c84be2 100644 --- a/activerecord/test/cases/validations/uniqueness_validation_test.rb +++ b/activerecord/test/cases/validations/uniqueness_validation_test.rb @@ -34,7 +34,22 @@ class TopicWithUniqEvent < Topic validates :event, uniqueness: true end +class BigIntTest < ActiveRecord::Base + INT_MAX_VALUE = 2147483647 + self.table_name = 'cars' + validates :engines_count, uniqueness: true, inclusion: { in: 0..INT_MAX_VALUE } +end + +class BigIntReverseTest < ActiveRecord::Base + INT_MAX_VALUE = 2147483647 + self.table_name = 'cars' + validates :engines_count, inclusion: { in: 0..INT_MAX_VALUE } + validates :engines_count, uniqueness: true +end + class UniquenessValidationTest < ActiveRecord::TestCase + INT_MAX_VALUE = 2147483647 + fixtures :topics, 'warehouse-things' repair_validations(Topic, Reply) @@ -86,6 +101,16 @@ class UniquenessValidationTest < ActiveRecord::TestCase assert t2.errors[:title] end + def test_validate_uniqueness_when_integer_out_of_range + entry = BigIntTest.create(engines_count: INT_MAX_VALUE + 1) + assert_equal entry.errors[:engines_count], ['is not included in the list'] + end + + def test_validate_uniqueness_when_integer_out_of_range_show_order_does_not_matter + entry = BigIntReverseTest.create(engines_count: INT_MAX_VALUE + 1) + assert_equal entry.errors[:engines_count], ['is not included in the list'] + end + def test_validates_uniqueness_with_newline_chars Topic.validates_uniqueness_of(:title, :case_sensitive => false) diff --git a/activerecord/test/cases/yaml_serialization_test.rb b/activerecord/test/cases/yaml_serialization_test.rb index bce59b4fcd..56909a8630 100644 --- a/activerecord/test/cases/yaml_serialization_test.rb +++ b/activerecord/test/cases/yaml_serialization_test.rb @@ -83,4 +83,39 @@ class YamlSerializationTest < ActiveRecord::TestCase assert_equal 5, author.posts_count assert_equal 5, dumped.posts_count end + + def test_a_yaml_version_is_provided_for_future_backwards_compat + coder = {} + Topic.first.encode_with(coder) + + assert coder['active_record_yaml_version'] + end + + def test_deserializing_rails_41_yaml + topic = YAML.load(yaml_fixture("rails_4_1")) + + assert topic.new_record? + assert_equal nil, topic.id + assert_equal "The First Topic", topic.title + assert_equal({ omg: :lol }, topic.content) + end + + def test_deserializing_rails_4_2_0_yaml + topic = YAML.load(yaml_fixture("rails_4_2_0")) + + assert_not topic.new_record? + assert_equal 1, topic.id + assert_equal "The First Topic", topic.title + assert_equal("Have a nice day", topic.content) + end + + private + + def yaml_fixture(file_name) + path = File.expand_path( + "../../support/yaml_compatibility_fixtures/#{file_name}.yml", + __FILE__ + ) + File.read(path) + end end diff --git a/activerecord/test/models/chef.rb b/activerecord/test/models/chef.rb index 67a4e54f06..698a52e045 100644 --- a/activerecord/test/models/chef.rb +++ b/activerecord/test/models/chef.rb @@ -1,3 +1,4 @@ class Chef < ActiveRecord::Base belongs_to :employable, polymorphic: true + has_many :recipes end diff --git a/activerecord/test/models/hotel.rb b/activerecord/test/models/hotel.rb index b352cd22f3..491f8dfde3 100644 --- a/activerecord/test/models/hotel.rb +++ b/activerecord/test/models/hotel.rb @@ -3,4 +3,5 @@ class Hotel < ActiveRecord::Base has_many :chefs, through: :departments has_many :cake_designers, source_type: 'CakeDesigner', source: :employable, through: :chefs has_many :drink_designers, source_type: 'DrinkDesigner', source: :employable, through: :chefs + has_many :recipes, through: :chefs end diff --git a/activerecord/test/models/recipe.rb b/activerecord/test/models/recipe.rb new file mode 100644 index 0000000000..c387230603 --- /dev/null +++ b/activerecord/test/models/recipe.rb @@ -0,0 +1,3 @@ +class Recipe < ActiveRecord::Base + belongs_to :chef +end diff --git a/activerecord/test/schema/mysql2_specific_schema.rb b/activerecord/test/schema/mysql2_specific_schema.rb index d6fd0c4ab0..52d3290c84 100644 --- a/activerecord/test/schema/mysql2_specific_schema.rb +++ b/activerecord/test/schema/mysql2_specific_schema.rb @@ -24,6 +24,11 @@ ActiveRecord::Schema.define do add_index :key_tests, :pizza, :using => :btree, :name => 'index_key_tests_on_pizza' add_index :key_tests, :snacks, :name => 'index_key_tests_on_snack' + create_table :collation_tests, id: false, force: true do |t| + t.string :string_cs_column, limit: 1, collation: 'utf8_bin' + t.string :string_ci_column, limit: 1, collation: 'utf8_general_ci' + end + ActiveRecord::Base.connection.execute <<-SQL DROP PROCEDURE IF EXISTS ten; SQL @@ -35,15 +40,6 @@ BEGIN END SQL - ActiveRecord::Base.connection.drop_table "collation_tests", if_exists: true - - ActiveRecord::Base.connection.execute <<-SQL -CREATE TABLE collation_tests ( - string_cs_column VARCHAR(1) COLLATE utf8_bin, - string_ci_column VARCHAR(1) COLLATE utf8_general_ci -) CHARACTER SET utf8 COLLATE utf8_general_ci -SQL - ActiveRecord::Base.connection.drop_table "enum_tests", if_exists: true ActiveRecord::Base.connection.execute <<-SQL diff --git a/activerecord/test/schema/mysql_specific_schema.rb b/activerecord/test/schema/mysql_specific_schema.rb index b5378341b5..90f5a60d7b 100644 --- a/activerecord/test/schema/mysql_specific_schema.rb +++ b/activerecord/test/schema/mysql_specific_schema.rb @@ -24,6 +24,11 @@ ActiveRecord::Schema.define do add_index :key_tests, :pizza, :using => :btree, :name => 'index_key_tests_on_pizza' add_index :key_tests, :snacks, :name => 'index_key_tests_on_snack' + create_table :collation_tests, id: false, force: true do |t| + t.string :string_cs_column, limit: 1, collation: 'utf8_bin' + t.string :string_ci_column, limit: 1, collation: 'utf8_general_ci' + end + ActiveRecord::Base.connection.execute <<-SQL DROP PROCEDURE IF EXISTS ten; SQL @@ -46,15 +51,6 @@ BEGIN END SQL - ActiveRecord::Base.connection.drop_table "collation_tests", if_exists: true - - ActiveRecord::Base.connection.execute <<-SQL -CREATE TABLE collation_tests ( - string_cs_column VARCHAR(1) COLLATE utf8_bin, - string_ci_column VARCHAR(1) COLLATE utf8_general_ci -) CHARACTER SET utf8 COLLATE utf8_general_ci -SQL - ActiveRecord::Base.connection.drop_table "enum_tests", if_exists: true ActiveRecord::Base.connection.execute <<-SQL diff --git a/activerecord/test/schema/postgresql_specific_schema.rb b/activerecord/test/schema/postgresql_specific_schema.rb index f84be0e7f4..008503bc24 100644 --- a/activerecord/test/schema/postgresql_specific_schema.rb +++ b/activerecord/test/schema/postgresql_specific_schema.rb @@ -93,4 +93,8 @@ _SQL t.binary :binary, limit: 100_000 t.text :text, limit: 100_000 end + + create_table :bigint_array, force: true do |t| + t.integer :big_int_data_points, limit: 8, array: true + end end diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index 5e5f7a798e..7b42f8a4a5 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -886,6 +886,10 @@ ActiveRecord::Schema.define do t.string :employable_type t.integer :department_id end + create_table :recipes, force: true do |t| + t.integer :chef_id + t.integer :hotel_id + end create_table :records, force: true do |t| end diff --git a/activerecord/test/support/yaml_compatibility_fixtures/rails_4_1.yml b/activerecord/test/support/yaml_compatibility_fixtures/rails_4_1.yml new file mode 100644 index 0000000000..20b128db9c --- /dev/null +++ b/activerecord/test/support/yaml_compatibility_fixtures/rails_4_1.yml @@ -0,0 +1,22 @@ +--- !ruby/object:Topic + attributes: + id: + title: The First Topic + author_name: David + author_email_address: david@loudthinking.com + written_on: 2003-07-16 14:28:11.223300000 Z + bonus_time: 2000-01-01 14:28:00.000000000 Z + last_read: 2004-04-15 + content: | + --- + :omg: :lol + important: + approved: false + replies_count: 1 + unique_replies_count: 0 + parent_id: + parent_title: + type: + group: + created_at: 2015-03-10 17:05:42.000000000 Z + updated_at: 2015-03-10 17:05:42.000000000 Z diff --git a/activerecord/test/support/yaml_compatibility_fixtures/rails_4_2_0.yml b/activerecord/test/support/yaml_compatibility_fixtures/rails_4_2_0.yml new file mode 100644 index 0000000000..b3d3b33141 --- /dev/null +++ b/activerecord/test/support/yaml_compatibility_fixtures/rails_4_2_0.yml @@ -0,0 +1,182 @@ +--- !ruby/object:Topic +raw_attributes: + id: 1 + title: The First Topic + author_name: David + author_email_address: david@loudthinking.com + written_on: '2003-07-16 14:28:11.223300' + bonus_time: '2005-01-30 14:28:00.000000' + last_read: '2004-04-15' + content: | + --- Have a nice day + ... + important: + approved: f + replies_count: 1 + unique_replies_count: 0 + parent_id: + parent_title: + type: + group: + created_at: '2015-03-10 17:44:41' + updated_at: '2015-03-10 17:44:41' +attributes: !ruby/object:ActiveRecord::AttributeSet + attributes: !ruby/object:ActiveRecord::LazyAttributeHash + types: + id: &5 !ruby/object:ActiveRecord::Type::Integer + precision: + scale: + limit: + range: !ruby/range + begin: -2147483648 + end: 2147483648 + excl: true + title: &6 !ruby/object:ActiveRecord::Type::String + precision: + scale: + limit: 250 + author_name: &1 !ruby/object:ActiveRecord::Type::String + precision: + scale: + limit: + author_email_address: *1 + written_on: &4 !ruby/object:ActiveRecord::Type::DateTime + precision: + scale: + limit: + bonus_time: &7 !ruby/object:ActiveRecord::Type::Time + precision: + scale: + limit: + last_read: &8 !ruby/object:ActiveRecord::Type::Date + precision: + scale: + limit: + content: !ruby/object:ActiveRecord::Type::Serialized + coder: &9 !ruby/object:ActiveRecord::Coders::YAMLColumn + object_class: !ruby/class 'Object' + subtype: &2 !ruby/object:ActiveRecord::Type::Text + precision: + scale: + limit: + important: *2 + approved: &10 !ruby/object:ActiveRecord::Type::Boolean + precision: + scale: + limit: + replies_count: &3 !ruby/object:ActiveRecord::Type::Integer + precision: + scale: + limit: + range: !ruby/range + begin: -2147483648 + end: 2147483648 + excl: true + unique_replies_count: *3 + parent_id: *3 + parent_title: *1 + type: *1 + group: *1 + created_at: *4 + updated_at: *4 + values: + id: 1 + title: The First Topic + author_name: David + author_email_address: david@loudthinking.com + written_on: '2003-07-16 14:28:11.223300' + bonus_time: '2005-01-30 14:28:00.000000' + last_read: '2004-04-15' + content: | + --- Have a nice day + ... + important: + approved: f + replies_count: 1 + unique_replies_count: 0 + parent_id: + parent_title: + type: + group: + created_at: '2015-03-10 17:44:41' + updated_at: '2015-03-10 17:44:41' + additional_types: {} + materialized: true + delegate_hash: + id: !ruby/object:ActiveRecord::Attribute::FromDatabase + name: id + value_before_type_cast: 1 + type: *5 + title: !ruby/object:ActiveRecord::Attribute::FromDatabase + name: title + value_before_type_cast: The First Topic + type: *6 + author_name: !ruby/object:ActiveRecord::Attribute::FromDatabase + name: author_name + value_before_type_cast: David + type: *1 + author_email_address: !ruby/object:ActiveRecord::Attribute::FromDatabase + name: author_email_address + value_before_type_cast: david@loudthinking.com + type: *1 + written_on: !ruby/object:ActiveRecord::Attribute::FromDatabase + name: written_on + value_before_type_cast: '2003-07-16 14:28:11.223300' + type: *4 + bonus_time: !ruby/object:ActiveRecord::Attribute::FromDatabase + name: bonus_time + value_before_type_cast: '2005-01-30 14:28:00.000000' + type: *7 + last_read: !ruby/object:ActiveRecord::Attribute::FromDatabase + name: last_read + value_before_type_cast: '2004-04-15' + type: *8 + content: !ruby/object:ActiveRecord::Attribute::FromDatabase + name: content + value_before_type_cast: | + --- Have a nice day + ... + type: !ruby/object:ActiveRecord::Type::Serialized + coder: *9 + subtype: *2 + important: !ruby/object:ActiveRecord::Attribute::FromDatabase + name: important + value_before_type_cast: + type: *2 + approved: !ruby/object:ActiveRecord::Attribute::FromDatabase + name: approved + value_before_type_cast: f + type: *10 + replies_count: !ruby/object:ActiveRecord::Attribute::FromDatabase + name: replies_count + value_before_type_cast: 1 + type: *3 + unique_replies_count: !ruby/object:ActiveRecord::Attribute::FromDatabase + name: unique_replies_count + value_before_type_cast: 0 + type: *3 + parent_id: !ruby/object:ActiveRecord::Attribute::FromDatabase + name: parent_id + value_before_type_cast: + type: *3 + parent_title: !ruby/object:ActiveRecord::Attribute::FromDatabase + name: parent_title + value_before_type_cast: + type: *1 + type: !ruby/object:ActiveRecord::Attribute::FromDatabase + name: type + value_before_type_cast: + type: *1 + group: !ruby/object:ActiveRecord::Attribute::FromDatabase + name: group + value_before_type_cast: + type: *1 + created_at: !ruby/object:ActiveRecord::Attribute::FromDatabase + name: created_at + value_before_type_cast: '2015-03-10 17:44:41' + type: *4 + updated_at: !ruby/object:ActiveRecord::Attribute::FromDatabase + name: updated_at + value_before_type_cast: '2015-03-10 17:44:41' + type: *4 +new_record: false diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md index f5fa70494a..ac27dc640e 100644 --- a/activesupport/CHANGELOG.md +++ b/activesupport/CHANGELOG.md @@ -1,12 +1,43 @@ -* Take DST into account when locating TimeZone from Numeric. +* Encoding ActiveSupport::TimeWithZone to YAML now preserves the timezone information. - When given a specific offset, use the first result found where the - total current offset (including any periodic deviations such as DST) - from UTC is equal. + Fixes #9183. - Fixes #15209. + *Andrew White* - *Yasyf Mohamedali* +* Added `ActiveSupport::TimeZone#strptime` to allow parsing times as if + from a given timezone. + + *Paul A Jungwirth* + +* `ActiveSupport::Callbacks#skip_callback` now raises an `ArgumentError` if + an unrecognized callback is removed. + + *Iain Beeston* + +* Added `ActiveSupport::ArrayInquirer` and `Array#inquiry`. + + Wrapping an array in an `ArrayInquirer` gives a friendlier way to check its + contents: + + variants = ActiveSupport::ArrayInquirer.new([:phone, :tablet]) + + variants.phone? # => true + variants.tablet? # => true + variants.desktop? # => false + + variants.any?(:phone, :tablet) # => true + variants.any?(:phone, :desktop) # => true + variants.any?(:desktop, :watch) # => false + + `Array#inquiry` is a shortcut for wrapping the receiving array in an + `ArrayInquirer`. + + *George Claghorn* + +* Deprecate `alias_method_chain` in favour of `Module#prepend` introduced in + Ruby 2.0. + + *Kir Shatrov* * Added `#without` on `Enumerable` and `Array` to return a copy of an enumerable without the specified elements. diff --git a/activesupport/activesupport.gemspec b/activesupport/activesupport.gemspec index a5339e6475..12c6a4d449 100644 --- a/activesupport/activesupport.gemspec +++ b/activesupport/activesupport.gemspec @@ -7,7 +7,7 @@ Gem::Specification.new do |s| s.summary = 'A toolkit of support libraries and Ruby core extensions extracted from the Rails framework.' s.description = 'A toolkit of support libraries and Ruby core extensions extracted from the Rails framework. Rich support for multibyte strings, internationalization, time zones, and testing.' - s.required_ruby_version = '>= 2.2.0' + s.required_ruby_version = '>= 2.2.2' s.license = 'MIT' diff --git a/activesupport/lib/active_support.rb b/activesupport/lib/active_support.rb index 290920dbf8..588d6c49f9 100644 --- a/activesupport/lib/active_support.rb +++ b/activesupport/lib/active_support.rb @@ -59,6 +59,7 @@ module ActiveSupport autoload :StringInquirer autoload :TaggedLogging autoload :XmlMini + autoload :ArrayInquirer end autoload :Rescuable @@ -72,6 +73,14 @@ module ActiveSupport end cattr_accessor :test_order # :nodoc: + + def self.halt_callback_chains_on_return_false + Callbacks::CallbackChain.halt_and_display_warning_on_return_false + end + + def self.halt_callback_chains_on_return_false=(value) + Callbacks::CallbackChain.halt_and_display_warning_on_return_false = value + end end autoload :I18n, "active_support/i18n" diff --git a/activesupport/lib/active_support/array_inquirer.rb b/activesupport/lib/active_support/array_inquirer.rb new file mode 100644 index 0000000000..0ae534da00 --- /dev/null +++ b/activesupport/lib/active_support/array_inquirer.rb @@ -0,0 +1,38 @@ +module ActiveSupport + # Wrapping an array in an +ArrayInquirer+ gives a friendlier way to check + # its string-like contents: + # + # variants = ActiveSupport::ArrayInquirer.new([:phone, :tablet]) + # + # variants.phone? # => true + # variants.tablet? # => true + # variants.desktop? # => false + # + # variants.any?(:phone, :tablet) # => true + # variants.any?(:phone, :desktop) # => true + # variants.any?(:desktop, :watch) # => false + class ArrayInquirer < Array + def any?(*candidates, &block) + if candidates.none? + super + else + candidates.any? do |candidate| + include?(candidate) || include?(candidate.to_sym) + end + end + end + + private + def respond_to_missing?(name, include_private = false) + name[-1] == '?' + end + + def method_missing(name, *args) + if name[-1] == '?' + any?(name[0..-2]) + else + super + end + end + end +end diff --git a/activesupport/lib/active_support/cache.rb b/activesupport/lib/active_support/cache.rb index 625be2c959..837974bc85 100644 --- a/activesupport/lib/active_support/cache.rb +++ b/activesupport/lib/active_support/cache.rb @@ -566,8 +566,8 @@ module ActiveSupport def handle_expired_entry(entry, key, options) if entry && entry.expired? race_ttl = options[:race_condition_ttl].to_i - if race_ttl && (Time.now.to_f - entry.expires_at <= race_ttl) - # When an entry has :race_condition_ttl defined, put the stale entry back into the cache + if (race_ttl > 0) && (Time.now.to_f - entry.expires_at <= race_ttl) + # When an entry has a positive :race_condition_ttl defined, put the stale entry back into the cache # for a brief period while the entry is being recalculated. entry.expires_at = Time.now + race_ttl write_entry(key, entry, :expires_in => race_ttl * 2) diff --git a/activesupport/lib/active_support/cache/file_store.rb b/activesupport/lib/active_support/cache/file_store.rb index d08ecd2f7d..e6a8b84214 100644 --- a/activesupport/lib/active_support/cache/file_store.rb +++ b/activesupport/lib/active_support/cache/file_store.rb @@ -29,6 +29,7 @@ module ActiveSupport def clear(options = nil) root_dirs = Dir.entries(cache_path).reject {|f| (EXCLUDED_DIRS + [".gitkeep"]).include?(f)} FileUtils.rm_r(root_dirs.collect{|f| File.join(cache_path, f)}) + rescue Errno::ENOENT end # Preemptively iterates through all stored keys and removes the ones which have expired. diff --git a/activesupport/lib/active_support/callbacks.rb b/activesupport/lib/active_support/callbacks.rb index 37f9494272..814fd288cf 100644 --- a/activesupport/lib/active_support/callbacks.rb +++ b/activesupport/lib/active_support/callbacks.rb @@ -5,6 +5,7 @@ require 'active_support/core_ext/class/attribute' require 'active_support/core_ext/kernel/reporting' require 'active_support/core_ext/kernel/singleton_class' require 'active_support/core_ext/string/filters' +require 'active_support/deprecation' require 'thread' module ActiveSupport @@ -79,14 +80,10 @@ module ActiveSupport # save # end def run_callbacks(kind, &block) - send "_run_#{kind}_callbacks", &block - end - - private + callbacks = send("_#{kind}_callbacks") - def _run_callbacks(callbacks, &block) if callbacks.empty? - block.call if block + yield if block_given? else runner = callbacks.compile e = Filters::Environment.new(self, false, nil, block) @@ -94,6 +91,8 @@ module ActiveSupport end end + private + # A hook invoked every time a before callback is halted. # This can be overridden in AS::Callback implementors in order # to provide better debugging/logging. @@ -662,10 +661,12 @@ module ActiveSupport # # ===== Options # - # * <tt>:if</tt> - A symbol naming an instance method or a proc; the - # callback will be called only when it returns a +true+ value. - # * <tt>:unless</tt> - A symbol naming an instance method or a proc; the - # callback will be called only when it returns a +false+ value. + # * <tt>:if</tt> - A symbol, a string or an array of symbols and strings, + # each naming an instance method or a proc; the callback will be called + # only when they all return a true value. + # * <tt>:unless</tt> - A symbol, a string or an array of symbols and + # strings, each naming an instance method or a proc; the callback will + # be called only when they all return a false value. # * <tt>:prepend</tt> - If +true+, the callback will be prepended to the # existing chain rather than appended. def set_callback(name, *filter_list, &block) @@ -688,19 +689,27 @@ module ActiveSupport # class Writer < Person # skip_callback :validate, :before, :check_membership, if: -> { self.age > 18 } # end + # + # An <tt>ArgumentError</tt> will be raised if the callback has not + # already been set (unless the <tt>:raise</tt> option is set to <tt>false</tt>). def skip_callback(name, *filter_list, &block) type, filters, options = normalize_callback_params(filter_list, block) + options[:raise] = true unless options.key?(:raise) __update_callbacks(name) do |target, chain| filters.each do |filter| - filter = chain.find {|c| c.matches?(type, filter) } + callback = chain.find {|c| c.matches?(type, filter) } + + if !callback && options[:raise] + raise ArgumentError, "#{type.to_s.capitalize} #{name} callback #{filter.inspect} has not been defined" + end - if filter && options.any? - new_filter = filter.merge_conditional_options(chain, if_option: options[:if], unless_option: options[:unless]) - chain.insert(chain.index(filter), new_filter) + if callback && (options.key?(:if) || options.key?(:unless)) + new_callback = callback.merge_conditional_options(chain, if_option: options[:if], unless_option: options[:unless]) + chain.insert(chain.index(callback), new_callback) end - chain.delete(filter) + chain.delete(callback) end target.set_callbacks name, chain end @@ -797,12 +806,6 @@ module ActiveSupport names.each do |name| class_attribute "_#{name}_callbacks" set_callbacks name, CallbackChain.new(name, options) - - module_eval <<-RUBY, __FILE__, __LINE__ + 1 - def _run_#{name}_callbacks(&block) - _run_callbacks(_#{name}_callbacks, &block) - end - RUBY end end diff --git a/activesupport/lib/active_support/core_ext/array.rb b/activesupport/lib/active_support/core_ext/array.rb index 7d0c1e4c8d..7551551bd7 100644 --- a/activesupport/lib/active_support/core_ext/array.rb +++ b/activesupport/lib/active_support/core_ext/array.rb @@ -4,3 +4,4 @@ require 'active_support/core_ext/array/conversions' require 'active_support/core_ext/array/extract_options' require 'active_support/core_ext/array/grouping' require 'active_support/core_ext/array/prepend_and_append' +require 'active_support/core_ext/array/inquiry' diff --git a/activesupport/lib/active_support/core_ext/array/conversions.rb b/activesupport/lib/active_support/core_ext/array/conversions.rb index 080e3b5ef7..d80df21e7d 100644 --- a/activesupport/lib/active_support/core_ext/array/conversions.rb +++ b/activesupport/lib/active_support/core_ext/array/conversions.rb @@ -74,7 +74,7 @@ class Array when 0 '' when 1 - self[0].to_s.dup + "#{self[0]}" when 2 "#{self[0]}#{options[:two_words_connector]}#{self[1]}" else diff --git a/activesupport/lib/active_support/core_ext/array/inquiry.rb b/activesupport/lib/active_support/core_ext/array/inquiry.rb new file mode 100644 index 0000000000..e8f44cc378 --- /dev/null +++ b/activesupport/lib/active_support/core_ext/array/inquiry.rb @@ -0,0 +1,17 @@ +require 'active_support/array_inquirer' + +class Array + # Wraps the array in an +ArrayInquirer+ object, which gives a friendlier way + # to check its string-like contents. + # + # pets = [:cat, :dog].inquiry + # + # pets.cat? # => true + # pets.ferret? # => false + # + # pets.any?(:cat, :ferret) # => true + # pets.any?(:ferret, :alligator) # => false + def inquiry + ActiveSupport::ArrayInquirer.new(self) + end +end diff --git a/activesupport/lib/active_support/core_ext/array/wrap.rb b/activesupport/lib/active_support/core_ext/array/wrap.rb index 152eb02218..b611d34c27 100644 --- a/activesupport/lib/active_support/core_ext/array/wrap.rb +++ b/activesupport/lib/active_support/core_ext/array/wrap.rb @@ -3,7 +3,7 @@ class Array # # Specifically: # - # * If the argument is +nil+ an empty list is returned. + # * If the argument is +nil+ an empty array is returned. # * Otherwise, if the argument responds to +to_ary+ it is invoked, and its result returned. # * Otherwise, returns an array with the argument as its single element. # @@ -15,12 +15,13 @@ class Array # # * If the argument responds to +to_ary+ the method is invoked. <tt>Kernel#Array</tt> # moves on to try +to_a+ if the returned value is +nil+, but <tt>Array.wrap</tt> returns - # +nil+ right away. + # an array with the argument as its single element right away. # * If the returned value from +to_ary+ is neither +nil+ nor an +Array+ object, <tt>Kernel#Array</tt> # raises an exception, while <tt>Array.wrap</tt> does not, it just returns the value. - # * It does not call +to_a+ on the argument, but returns an empty array if argument is +nil+. + # * It does not call +to_a+ on the argument, if the argument does not respond to +to_ary+ + # it returns an array with the argument as its single element. # - # The second point is easily explained with some enumerables: + # The last point is easily explained with some enumerables: # # Array(foo: :bar) # => [[:foo, :bar]] # Array.wrap(foo: :bar) # => [{:foo=>:bar}] diff --git a/activesupport/lib/active_support/core_ext/hash/keys.rb b/activesupport/lib/active_support/core_ext/hash/keys.rb index 9297a59c46..c30044b9ff 100644 --- a/activesupport/lib/active_support/core_ext/hash/keys.rb +++ b/activesupport/lib/active_support/core_ext/hash/keys.rb @@ -14,7 +14,7 @@ class Hash result end - # Destructively convert all keys using the block operations. + # Destructively converts all keys using the block operations. # Same as transform_keys but modifies +self+. def transform_keys! return enum_for(:transform_keys!) unless block_given? @@ -34,7 +34,7 @@ class Hash transform_keys(&:to_s) end - # Destructively convert all keys to strings. Same as + # Destructively converts all keys to strings. Same as # +stringify_keys+, but modifies +self+. def stringify_keys! transform_keys!(&:to_s) @@ -52,14 +52,14 @@ class Hash end alias_method :to_options, :symbolize_keys - # Destructively convert all keys to symbols, as long as they respond + # Destructively converts all keys to symbols, as long as they respond # to +to_sym+. Same as +symbolize_keys+, but modifies +self+. def symbolize_keys! transform_keys!{ |key| key.to_sym rescue key } end alias_method :to_options!, :symbolize_keys! - # Validate all keys in a hash match <tt>*valid_keys</tt>, raising + # Validates all keys in a hash match <tt>*valid_keys</tt>, raising # ArgumentError on a mismatch. # # Note that keys are treated differently than HashWithIndifferentAccess, @@ -89,7 +89,7 @@ class Hash _deep_transform_keys_in_object(self, &block) end - # Destructively convert all keys by using the block operation. + # Destructively converts all keys by using the block operation. # This includes the keys from the root hash and from all # nested hashes and arrays. def deep_transform_keys!(&block) @@ -108,7 +108,7 @@ class Hash deep_transform_keys(&:to_s) end - # Destructively convert all keys to strings. + # Destructively converts all keys to strings. # This includes the keys from the root hash and from all # nested hashes and arrays. def deep_stringify_keys! @@ -127,7 +127,7 @@ class Hash deep_transform_keys{ |key| key.to_sym rescue key } end - # Destructively convert all keys to symbols, as long as they respond + # Destructively converts all keys to symbols, as long as they respond # to +to_sym+. This includes the keys from the root hash and from all # nested hashes and arrays. def deep_symbolize_keys! diff --git a/activesupport/lib/active_support/core_ext/hash/slice.rb b/activesupport/lib/active_support/core_ext/hash/slice.rb index 41b2279013..1d5f38231a 100644 --- a/activesupport/lib/active_support/core_ext/hash/slice.rb +++ b/activesupport/lib/active_support/core_ext/hash/slice.rb @@ -1,5 +1,5 @@ class Hash - # Slice a hash to include only the given keys. Returns a hash containing + # Slices a hash to include only the given keys. Returns a hash containing # the given keys. # # { a: 1, b: 2, c: 3, d: 4 }.slice(:a, :b) diff --git a/activesupport/lib/active_support/core_ext/integer/time.rb b/activesupport/lib/active_support/core_ext/integer/time.rb index 82080ffe51..f0b7382ef3 100644 --- a/activesupport/lib/active_support/core_ext/integer/time.rb +++ b/activesupport/lib/active_support/core_ext/integer/time.rb @@ -17,21 +17,6 @@ class Integer # # # equivalent to Time.now.advance(months: 4, years: 5) # (4.months + 5.years).from_now - # - # While these methods provide precise calculation when used as in the examples - # above, care should be taken to note that this is not true if the result of - # +months+, +years+, etc is converted before use: - # - # # equivalent to 30.days.to_i.from_now - # 1.month.to_i.from_now - # - # # equivalent to 365.25.days.to_f.from_now - # 1.year.to_f.from_now - # - # In such cases, Ruby's core - # Date[http://ruby-doc.org/stdlib/libdoc/date/rdoc/Date.html] and - # Time[http://ruby-doc.org/stdlib/libdoc/time/rdoc/Time.html] should be used for precision - # date and time arithmetic. def months ActiveSupport::Duration.new(self * 30.days, [[:months, self]]) end diff --git a/activesupport/lib/active_support/core_ext/marshal.rb b/activesupport/lib/active_support/core_ext/marshal.rb index 56c79c04bd..20a0856e71 100644 --- a/activesupport/lib/active_support/core_ext/marshal.rb +++ b/activesupport/lib/active_support/core_ext/marshal.rb @@ -1,9 +1,7 @@ -require 'active_support/core_ext/module/aliasing' - -module Marshal - class << self - def load_with_autoloading(source) - load_without_autoloading(source) +module ActiveSupport + module MarshalWithAutoloading # :nodoc: + def load(source) + super(source) rescue ArgumentError, NameError => exc if exc.message.match(%r|undefined class/module (.+)|) # try loading the class/module @@ -15,7 +13,7 @@ module Marshal raise exc end end - - alias_method_chain :load, :autoloading end end + +Marshal.singleton_class.prepend(ActiveSupport::MarshalWithAutoloading) diff --git a/activesupport/lib/active_support/core_ext/module/aliasing.rb b/activesupport/lib/active_support/core_ext/module/aliasing.rb index 0a6fadf928..b6934b9c54 100644 --- a/activesupport/lib/active_support/core_ext/module/aliasing.rb +++ b/activesupport/lib/active_support/core_ext/module/aliasing.rb @@ -1,4 +1,7 @@ class Module + # NOTE: This method is deprecated. Please use <tt>Module#prepend</tt> that + # comes with Ruby 2.0 or newer instead. + # # Encapsulates the common pattern of: # # alias_method :foo_without_feature, :foo @@ -21,6 +24,8 @@ class Module # # so you can safely chain foo, foo?, foo! and/or foo= with the same feature. def alias_method_chain(target, feature) + ActiveSupport::Deprecation.warn("alias_method_chain is deprecated. Please, use Module#prepend instead. From module, you can access the original method using super.") + # Strip out punctuation on predicates, bang or writer methods since # e.g. target?_without_feature is not a valid method name. aliased_target, punctuation = target.to_s.sub(/([?!=])$/, ''), $1 @@ -43,7 +48,7 @@ class Module end # Allows you to make aliases for attributes, which includes - # getter, setter, and query methods. + # getter, setter, and a predicate. # # class Content < ActiveRecord::Base # # has a title attribute diff --git a/activesupport/lib/active_support/core_ext/module/concerning.rb b/activesupport/lib/active_support/core_ext/module/concerning.rb index 07a392404e..e26b594fc4 100644 --- a/activesupport/lib/active_support/core_ext/module/concerning.rb +++ b/activesupport/lib/active_support/core_ext/module/concerning.rb @@ -63,10 +63,10 @@ class Module # # == Mix-in noise exiled to its own file: # - # Once our chunk of behavior starts pushing the scroll-to-understand it's + # Once our chunk of behavior starts pushing the scroll-to-understand-it # boundary, we give in and move it to a separate file. At this size, the - # overhead feels in good proportion to the size of our extraction, despite - # diluting our at-a-glance sense of how things really work. + # increased overhead can be a reasonable tradeoff even if it reduces our + # at-a-glance perception of how things work. # # class Todo # # Other todo implementation diff --git a/activesupport/lib/active_support/core_ext/numeric/time.rb b/activesupport/lib/active_support/core_ext/numeric/time.rb index 98716383f4..6c4a975495 100644 --- a/activesupport/lib/active_support/core_ext/numeric/time.rb +++ b/activesupport/lib/active_support/core_ext/numeric/time.rb @@ -18,21 +18,6 @@ class Numeric # # # equivalent to Time.current.advance(months: 4, years: 5) # (4.months + 5.years).from_now - # - # While these methods provide precise calculation when used as in the examples above, care - # should be taken to note that this is not true if the result of `months', `years', etc is - # converted before use: - # - # # equivalent to 30.days.to_i.from_now - # 1.month.to_i.from_now - # - # # equivalent to 365.25.days.to_f.from_now - # 1.year.to_f.from_now - # - # In such cases, Ruby's core - # Date[http://ruby-doc.org/stdlib/libdoc/date/rdoc/Date.html] and - # Time[http://ruby-doc.org/stdlib/libdoc/time/rdoc/Time.html] should be used for precision - # date and time arithmetic. def seconds ActiveSupport::Duration.new(self, [[:seconds, self]]) end diff --git a/activesupport/lib/active_support/core_ext/object/json.rb b/activesupport/lib/active_support/core_ext/object/json.rb index 698b2d1920..0db787010c 100644 --- a/activesupport/lib/active_support/core_ext/object/json.rb +++ b/activesupport/lib/active_support/core_ext/object/json.rb @@ -9,7 +9,6 @@ require 'time' require 'active_support/core_ext/time/conversions' require 'active_support/core_ext/date_time/conversions' require 'active_support/core_ext/date/conversions' -require 'active_support/core_ext/module/aliasing' # The JSON gem adds a few modules to Ruby core classes containing :to_json definition, overwriting # their default behavior. That said, we need to define the basic to_json method in all of them, @@ -26,22 +25,25 @@ require 'active_support/core_ext/module/aliasing' # bypassed completely. This means that as_json won't be invoked and the JSON gem will simply # ignore any options it does not natively understand. This also means that ::JSON.{generate,dump} # should give exactly the same results with or without active support. -[Object, Array, FalseClass, Float, Hash, Integer, NilClass, String, TrueClass, Enumerable].each do |klass| - klass.class_eval do - def to_json_with_active_support_encoder(options = nil) + +module ActiveSupport + module ToJsonWithActiveSupportEncoder # :nodoc: + def to_json(options = nil) if options.is_a?(::JSON::State) # Called from JSON.{generate,dump}, forward it to JSON gem's to_json - self.to_json_without_active_support_encoder(options) + super(options) else # to_json is being invoked directly, use ActiveSupport's encoder ActiveSupport::JSON.encode(self, options) end end - - alias_method_chain :to_json, :active_support_encoder end end +[Object, Array, FalseClass, Float, Hash, Integer, NilClass, String, TrueClass, Enumerable].reverse_each do |klass| + klass.prepend(ActiveSupport::ToJsonWithActiveSupportEncoder) +end + class Object def as_json(options = nil) #:nodoc: if respond_to?(:to_hash) 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 7d38e1d134..513c8b1d55 100644 --- a/activesupport/lib/active_support/core_ext/object/with_options.rb +++ b/activesupport/lib/active_support/core_ext/object/with_options.rb @@ -7,7 +7,7 @@ class Object # provided. Each method called on the block variable must take an options # hash as its final argument. # - # Without <tt>with_options></tt>, this code contains duplication: + # Without <tt>with_options</tt>, this code contains duplication: # # class Account < ActiveRecord::Base # has_many :customers, dependent: :destroy diff --git a/activesupport/lib/active_support/core_ext/range/each.rb b/activesupport/lib/active_support/core_ext/range/each.rb index ecef78f55f..dc6dad5ced 100644 --- a/activesupport/lib/active_support/core_ext/range/each.rb +++ b/activesupport/lib/active_support/core_ext/range/each.rb @@ -1,23 +1,21 @@ -require 'active_support/core_ext/module/aliasing' +module ActiveSupport + module EachTimeWithZone #:nodoc: + def each(&block) + ensure_iteration_allowed + super + end -class Range #:nodoc: + def step(n = 1, &block) + ensure_iteration_allowed + super + end - def each_with_time_with_zone(&block) - ensure_iteration_allowed - each_without_time_with_zone(&block) - end - alias_method_chain :each, :time_with_zone + private - def step_with_time_with_zone(n = 1, &block) - ensure_iteration_allowed - step_without_time_with_zone(n, &block) - end - alias_method_chain :step, :time_with_zone - - private - def ensure_iteration_allowed - if first.is_a?(Time) - raise TypeError, "can't iterate from #{first.class}" - end + def ensure_iteration_allowed + raise TypeError, "can't iterate from #{first.class}" if first.is_a?(Time) + end end end + +Range.prepend(ActiveSupport::EachTimeWithZone) diff --git a/activesupport/lib/active_support/core_ext/range/include_range.rb b/activesupport/lib/active_support/core_ext/range/include_range.rb index 3a07401c8a..c69e1e3fb9 100644 --- a/activesupport/lib/active_support/core_ext/range/include_range.rb +++ b/activesupport/lib/active_support/core_ext/range/include_range.rb @@ -1,23 +1,23 @@ -require 'active_support/core_ext/module/aliasing' - -class Range - # Extends the default Range#include? to support range comparisons. - # (1..5).include?(1..5) # => true - # (1..5).include?(2..3) # => true - # (1..5).include?(2..6) # => false - # - # The native Range#include? behavior is untouched. - # ('a'..'f').include?('c') # => true - # (5..9).include?(11) # => false - def include_with_range?(value) - if value.is_a?(::Range) - # 1...10 includes 1..9 but it does not include 1..10. - operator = exclude_end? && !value.exclude_end? ? :< : :<= - include_without_range?(value.first) && value.last.send(operator, last) - else - include_without_range?(value) +module ActiveSupport + module IncludeWithRange #:nodoc: + # Extends the default Range#include? to support range comparisons. + # (1..5).include?(1..5) # => true + # (1..5).include?(2..3) # => true + # (1..5).include?(2..6) # => false + # + # The native Range#include? behavior is untouched. + # ('a'..'f').include?('c') # => true + # (5..9).include?(11) # => false + def include?(value) + if value.is_a?(::Range) + # 1...10 includes 1..9 but it does not include 1..10. + operator = exclude_end? && !value.exclude_end? ? :< : :<= + super(value.first) && value.last.send(operator, last) + else + super + end end end - - alias_method_chain :include?, :range end + +Range.prepend(ActiveSupport::IncludeWithRange) diff --git a/activesupport/lib/active_support/core_ext/string/behavior.rb b/activesupport/lib/active_support/core_ext/string/behavior.rb index 4aa960039b..710f1f4670 100644 --- a/activesupport/lib/active_support/core_ext/string/behavior.rb +++ b/activesupport/lib/active_support/core_ext/string/behavior.rb @@ -1,5 +1,5 @@ class String - # Enable more predictable duck-typing on String-like classes. See <tt>Object#acts_like?</tt>. + # Enables more predictable duck-typing on String-like classes. See <tt>Object#acts_like?</tt>. def acts_like_string? true end diff --git a/activesupport/lib/active_support/core_ext/string/filters.rb b/activesupport/lib/active_support/core_ext/string/filters.rb index 7461d03acc..375ec1aef8 100644 --- a/activesupport/lib/active_support/core_ext/string/filters.rb +++ b/activesupport/lib/active_support/core_ext/string/filters.rb @@ -17,9 +17,8 @@ class String # str.squish! # => "foo bar boo" # str # => "foo bar boo" def squish! - gsub!(/\A[[:space:]]+/, '') - gsub!(/[[:space:]]+\z/, '') gsub!(/[[:space:]]+/, ' ') + strip! self end diff --git a/activesupport/lib/active_support/core_ext/string/inflections.rb b/activesupport/lib/active_support/core_ext/string/inflections.rb index 38d567c014..97f9720b2b 100644 --- a/activesupport/lib/active_support/core_ext/string/inflections.rb +++ b/activesupport/lib/active_support/core_ext/string/inflections.rb @@ -178,7 +178,7 @@ class String ActiveSupport::Inflector.tableize(self) end - # Create a class name from a plural table name like Rails does for table names to models. + # Creates a class name from a plural table name like Rails does for table names to models. # Note that this returns a string and not a class. (To convert to an actual class # follow +classify+ with +constantize+.) # diff --git a/activesupport/lib/active_support/core_ext/string/multibyte.rb b/activesupport/lib/active_support/core_ext/string/multibyte.rb index 57d7f8d1e7..7055f7f699 100644 --- a/activesupport/lib/active_support/core_ext/string/multibyte.rb +++ b/activesupport/lib/active_support/core_ext/string/multibyte.rb @@ -35,7 +35,7 @@ class String ActiveSupport::Multibyte.proxy_class.new(self) end - # Return +true+ if string has utf_8 encoding. + # Returns +true+ if string has utf_8 encoding. # # utf_8_str = "some string".encode "UTF-8" # iso_str = "some string".encode "ISO-8859-1" 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 bae4e206e6..c676b26b06 100644 --- a/activesupport/lib/active_support/core_ext/string/output_safety.rb +++ b/activesupport/lib/active_support/core_ext/string/output_safety.rb @@ -13,7 +13,7 @@ class ERB # This method is also aliased as <tt>h</tt>. # # In your ERB templates, use this method to escape any unsafe content. For example: - # <%=h @person.name %> + # <%= h @person.name %> # # puts html_escape('is a > 0 & a < 10?') # # => is a > 0 & a < 10? diff --git a/activesupport/lib/active_support/core_ext/time/calculations.rb b/activesupport/lib/active_support/core_ext/time/calculations.rb index 6f1b653639..1ce68ea7c7 100644 --- a/activesupport/lib/active_support/core_ext/time/calculations.rb +++ b/activesupport/lib/active_support/core_ext/time/calculations.rb @@ -246,8 +246,10 @@ class Time # Layers additional behavior on Time#<=> so that DateTime and ActiveSupport::TimeWithZone instances # can be chronologically compared with a Time def compare_with_coercion(other) - # we're avoiding Time#to_datetime cause it's expensive - if other.is_a?(Time) + # we're avoiding Time#to_datetime and Time#to_time because they're expensive + if other.class == Time + compare_without_coercion(other) + elsif other.is_a?(Time) compare_without_coercion(other.to_time) else to_datetime <=> other diff --git a/activesupport/lib/active_support/deprecation/method_wrappers.rb b/activesupport/lib/active_support/deprecation/method_wrappers.rb index cab8a1b14d..c74e9c40ac 100644 --- a/activesupport/lib/active_support/deprecation/method_wrappers.rb +++ b/activesupport/lib/active_support/deprecation/method_wrappers.rb @@ -31,12 +31,14 @@ module ActiveSupport method_names += options.keys method_names.each do |method_name| - target_module.alias_method_chain(method_name, :deprecation) do |target, punctuation| - target_module.send(:define_method, "#{target}_with_deprecation#{punctuation}") do |*args, &block| + mod = Module.new do + define_method(method_name) do |*args, &block| deprecator.deprecation_warning(method_name, options[method_name]) - send(:"#{target}_without_deprecation#{punctuation}", *args, &block) + super(*args, &block) end end + + target_module.prepend(mod) end end end diff --git a/activesupport/lib/active_support/duration.rb b/activesupport/lib/active_support/duration.rb index 5a64fc52cc..4c0d1197fe 100644 --- a/activesupport/lib/active_support/duration.rb +++ b/activesupport/lib/active_support/duration.rb @@ -56,6 +56,30 @@ module ActiveSupport @value.to_s end + # Returns the number of seconds that this Duration represents. + # + # 1.minute.to_i # => 60 + # 1.hour.to_i # => 3600 + # 1.day.to_i # => 86400 + # + # Note that this conversion makes some assumptions about the + # duration of some periods, e.g. months are always 30 days + # and years are 365.25 days: + # + # # equivalent to 30.days.to_i + # 1.month.to_i # => 2592000 + # + # # equivalent to 365.25.days.to_i + # 1.year.to_i # => 31557600 + # + # In such cases, Ruby's core + # Date[http://ruby-doc.org/stdlib/libdoc/date/rdoc/Date.html] and + # Time[http://ruby-doc.org/stdlib/libdoc/time/rdoc/Time.html] should be used for precision + # date and time arithmetic. + def to_i + @value.to_i + end + # Returns +true+ if +other+ is also a Duration instance, which has the # same parts as this one. def eql?(other) diff --git a/activesupport/lib/active_support/inflector/methods.rb b/activesupport/lib/active_support/inflector/methods.rb index fe8a2ac9ba..a08c655d69 100644 --- a/activesupport/lib/active_support/inflector/methods.rb +++ b/activesupport/lib/active_support/inflector/methods.rb @@ -73,7 +73,7 @@ module ActiveSupport string = string.sub(/^(?:#{inflections.acronym_regex}(?=\b|[A-Z_])|\w)/) { $&.downcase } end string.gsub!(/(?:_|(\/))([a-z\d]*)/i) { "#{$1}#{inflections.acronyms[$2] || $2.capitalize}" } - string.gsub!(/\//, '::') + string.gsub!('/'.freeze, '::'.freeze) string end @@ -90,7 +90,7 @@ module ActiveSupport # camelize(underscore('SSLError')) # => "SslError" def underscore(camel_cased_word) return camel_cased_word unless camel_cased_word =~ /[A-Z-]|::/ - word = camel_cased_word.to_s.gsub(/::/, '/') + word = camel_cased_word.to_s.gsub('::'.freeze, '/'.freeze) word.gsub!(/(?:(?<=([A-Za-z\d]))|\b)(#{inflections.acronym_regex})(?=\b|[^a-z])/) { "#{$1 && '_'}#{$2.downcase}" } word.gsub!(/([A-Z\d]+)([A-Z][a-z])/,'\1_\2') word.gsub!(/([a-z\d])([A-Z])/,'\1_\2') diff --git a/activesupport/lib/active_support/number_helper/number_to_currency_converter.rb b/activesupport/lib/active_support/number_helper/number_to_currency_converter.rb index ce03700de1..cd5a2b3cbb 100644 --- a/activesupport/lib/active_support/number_helper/number_to_currency_converter.rb +++ b/activesupport/lib/active_support/number_helper/number_to_currency_converter.rb @@ -13,7 +13,7 @@ module ActiveSupport end rounded_number = NumberToRoundedConverter.convert(number, options) - format.gsub(/%n/, rounded_number).gsub(/%u/, options[:unit]) + format.gsub('%n'.freeze, rounded_number).gsub('%u'.freeze, options[:unit]) end private diff --git a/activesupport/lib/active_support/number_helper/number_to_human_converter.rb b/activesupport/lib/active_support/number_helper/number_to_human_converter.rb index 6940beb318..5c6fe2df83 100644 --- a/activesupport/lib/active_support/number_helper/number_to_human_converter.rb +++ b/activesupport/lib/active_support/number_helper/number_to_human_converter.rb @@ -23,7 +23,7 @@ module ActiveSupport unit = determine_unit(units, exponent) rounded_number = NumberToRoundedConverter.convert(number, options) - format.gsub(/%n/, rounded_number).gsub(/%u/, unit).strip + format.gsub('%n'.freeze, rounded_number).gsub('%u'.freeze, unit).strip end private diff --git a/activesupport/lib/active_support/number_helper/number_to_human_size_converter.rb b/activesupport/lib/active_support/number_helper/number_to_human_size_converter.rb index 78d2c9ae6e..ac0d20b454 100644 --- a/activesupport/lib/active_support/number_helper/number_to_human_size_converter.rb +++ b/activesupport/lib/active_support/number_helper/number_to_human_size_converter.rb @@ -20,7 +20,7 @@ module ActiveSupport human_size = number / (base ** exponent) number_to_format = NumberToRoundedConverter.convert(human_size, options) end - conversion_format.gsub(/%n/, number_to_format).gsub(/%u/, unit) + conversion_format.gsub('%n'.freeze, number_to_format).gsub('%u'.freeze, unit) end private diff --git a/activesupport/lib/active_support/number_helper/number_to_percentage_converter.rb b/activesupport/lib/active_support/number_helper/number_to_percentage_converter.rb index 1af294a03e..4c04d40c19 100644 --- a/activesupport/lib/active_support/number_helper/number_to_percentage_converter.rb +++ b/activesupport/lib/active_support/number_helper/number_to_percentage_converter.rb @@ -5,7 +5,7 @@ module ActiveSupport def convert rounded_number = NumberToRoundedConverter.convert(number, options) - options[:format].gsub(/%n/, rounded_number) + options[:format].gsub('%n'.freeze, rounded_number) end end end diff --git a/activesupport/lib/active_support/railtie.rb b/activesupport/lib/active_support/railtie.rb index ef22433491..cd0fb51009 100644 --- a/activesupport/lib/active_support/railtie.rb +++ b/activesupport/lib/active_support/railtie.rb @@ -13,13 +13,6 @@ module ActiveSupport end end - initializer "active_support.halt_callback_chains_on_return_false", after: :load_config_initializers do |app| - if app.config.active_support.key? :halt_callback_chains_on_return_false - ActiveSupport::Callbacks::CallbackChain.halt_and_display_warning_on_return_false = \ - app.config.active_support.halt_callback_chains_on_return_false - end - end - # Sets the default value for Time.zone # If assigned value cannot be matched to a TimeZone, an exception will be raised. initializer "active_support.initialize_time_zone" do |app| diff --git a/activesupport/lib/active_support/subscriber.rb b/activesupport/lib/active_support/subscriber.rb index cd40284660..8db423f0e9 100644 --- a/activesupport/lib/active_support/subscriber.rb +++ b/activesupport/lib/active_support/subscriber.rb @@ -10,19 +10,14 @@ module ActiveSupport # # module ActiveRecord # class StatsSubscriber < ActiveSupport::Subscriber + # attach_to :active_record + # # def sql(event) # Statsd.timing("sql.#{event.payload[:name]}", event.duration) # end # end # end # - # And it's finally registered as: - # - # ActiveRecord::StatsSubscriber.attach_to :active_record - # - # Since we need to know all instance methods before attaching the log - # subscriber, the line above should be called after your subscriber definition. - # # After configured, whenever a "sql.active_record" notification is published, # it will properly dispatch the event (ActiveSupport::Notifications::Event) to # the +sql+ method. diff --git a/activesupport/lib/active_support/test_case.rb b/activesupport/lib/active_support/test_case.rb index 739823bd56..24b8f4b9f9 100644 --- a/activesupport/lib/active_support/test_case.rb +++ b/activesupport/lib/active_support/test_case.rb @@ -45,8 +45,6 @@ module ActiveSupport test_order end - - alias :my_tests_are_order_dependent! :i_suck_and_my_tests_are_order_dependent! end alias_method :method_name, :name @@ -75,7 +73,7 @@ module ActiveSupport alias :assert_not_respond_to :refute_respond_to alias :assert_not_same :refute_same - # Fails if the block raises an exception. + # Reveals the intention that the block should not raise any exception. # # assert_nothing_raised do # ... diff --git a/activesupport/lib/active_support/testing/isolation.rb b/activesupport/lib/active_support/testing/isolation.rb index 247df7423b..1de0a19998 100644 --- a/activesupport/lib/active_support/testing/isolation.rb +++ b/activesupport/lib/active_support/testing/isolation.rb @@ -69,17 +69,17 @@ module ActiveSupport else Tempfile.open("isolation") do |tmpfile| env = { - ISOLATION_TEST: self.class.name, - ISOLATION_OUTPUT: tmpfile.path + 'ISOLATION_TEST' => self.class.name, + 'ISOLATION_OUTPUT' => tmpfile.path } load_paths = $-I.map {|p| "-I\"#{File.expand_path(p)}\"" }.join(" ") orig_args = ORIG_ARGV.join(" ") test_opts = "-n#{self.class.name}##{self.name}" - command = "#{Gem.ruby} #{load_paths} #{$0} #{orig_args} #{test_opts}" + 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]) + child = IO.popen(env, command) begin Process.wait(child.pid) diff --git a/activesupport/lib/active_support/testing/time_helpers.rb b/activesupport/lib/active_support/testing/time_helpers.rb index df5186ddec..3478b09423 100644 --- a/activesupport/lib/active_support/testing/time_helpers.rb +++ b/activesupport/lib/active_support/testing/time_helpers.rb @@ -39,15 +39,16 @@ module ActiveSupport end end - # Containing helpers that helps you test passage of time. + # Contain helpers that help you test passage of time. module TimeHelpers # Changes current time to the time in the future or in the past by a given time difference by - # stubbing +Time.now+ and +Date.today+. + # stubbing +Time.now+, +Date.today+, and +DateTime.now+. # - # Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00 + # Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00 # travel 1.day - # Time.current # => Sun, 10 Nov 2013 15:34:49 EST -05:00 - # Date.current # => Sun, 10 Nov 2013 + # Time.current # => Sun, 10 Nov 2013 15:34:49 EST -05:00 + # Date.current # => Sun, 10 Nov 2013 + # DateTime.current # => Sun, 10 Nov 2013 15:34:49 -0500 # # This method also accepts a block, which will return the current time back to its original # state at the end of the block: @@ -61,13 +62,14 @@ module ActiveSupport travel_to Time.now + duration, &block end - # Changes current time to the given time by stubbing +Time.now+ and - # +Date.today+ to return the time or date passed into this method. + # Changes current time to the given time by stubbing +Time.now+, + # +Date.today+, and +DateTime.now+ to return the time or date passed into this method. # - # Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00 + # Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00 # travel_to Time.new(2004, 11, 24, 01, 04, 44) - # Time.current # => Wed, 24 Nov 2004 01:04:44 EST -05:00 - # Date.current # => Wed, 24 Nov 2004 + # Time.current # => Wed, 24 Nov 2004 01:04:44 EST -05:00 + # Date.current # => Wed, 24 Nov 2004 + # DateTime.current # => Wed, 24 Nov 2004 01:04:44 -0500 # # Dates are taken as their timestamp at the beginning of the day in the # application time zone. <tt>Time.current</tt> returns said timestamp, diff --git a/activesupport/lib/active_support/time_with_zone.rb b/activesupport/lib/active_support/time_with_zone.rb index c28de4e21c..b0d7f3299f 100644 --- a/activesupport/lib/active_support/time_with_zone.rb +++ b/activesupport/lib/active_support/time_with_zone.rb @@ -169,12 +169,13 @@ module ActiveSupport end end - def encode_with(coder) - if coder.respond_to?(:represent_object) - coder.represent_object(nil, utc) - else - coder.represent_scalar(nil, utc.strftime("%Y-%m-%d %H:%M:%S.%9NZ")) - end + def init_with(coder) #:nodoc: + initialize(coder['utc'], coder['zone'], coder['time']) + end + + def encode_with(coder) #:nodoc: + coder.tag = '!ruby/object:ActiveSupport::TimeWithZone' + coder.map = { 'utc' => utc, 'zone' => time_zone, 'time' => time } end # Returns a string of the object's date and time in the format used by diff --git a/activesupport/lib/active_support/values/time_zone.rb b/activesupport/lib/active_support/values/time_zone.rb index ab6a78052c..2699a064d7 100644 --- a/activesupport/lib/active_support/values/time_zone.rb +++ b/activesupport/lib/active_support/values/time_zone.rb @@ -239,7 +239,7 @@ module ActiveSupport end when Numeric, ActiveSupport::Duration arg *= 3600 if arg.abs <= 13 - all.find { |z| z.utc_total_offset == arg.to_i } + all.find { |z| z.utc_offset == arg.to_i } else raise ArgumentError, "invalid argument to TimeZone[]: #{arg.inspect}" end @@ -285,12 +285,6 @@ module ActiveSupport end end - # Returns the offset of this time zone from UTC in seconds, - # taking DST into account. - def utc_total_offset - tzinfo.current_period.utc_total_offset if tzinfo - end - # Returns the offset of this time zone as a formatted string, of the # format "+HH:MM". def formatted_offset(colon=true, alternate_utc_string = nil) @@ -354,24 +348,31 @@ module ActiveSupport # # Time.zone.parse('Mar 2000') # => Wed, 01 Mar 2000 00:00:00 HST -10:00 def parse(str, now=now()) - parts = Date._parse(str, false) - return if parts.empty? - - time = Time.new( - parts.fetch(:year, now.year), - parts.fetch(:mon, now.month), - parts.fetch(:mday, parts[:year] || parts[:mon] ? 1 : now.day), - parts.fetch(:hour, 0), - parts.fetch(:min, 0), - parts.fetch(:sec, 0) + parts.fetch(:sec_fraction, 0), - parts.fetch(:offset, 0) - ) - - if parts[:offset] - TimeWithZone.new(time.utc, self) - else - TimeWithZone.new(nil, self, time) - end + parts_to_time(Date._parse(str, false), now) + end + + # Parses +str+ according to +format+ and returns an ActiveSupport::TimeWithZone. + # + # Assumes that +str+ is a time in the time zone +self+, + # unless +format+ includes an explicit time zone. + # (This is the same behavior as +parse+.) + # In either case, the returned TimeWithZone has the timezone of +self+. + # + # Time.zone = 'Hawaii' # => "Hawaii" + # Time.zone.strptime('1999-12-31 14:00:00', '%Y-%m-%d %H:%M:%S') # => Fri, 31 Dec 1999 14:00:00 HST -10:00 + # + # If upper components are missing from the string, they are supplied from + # TimeZone#now: + # + # Time.zone.now # => Fri, 31 Dec 1999 14:00:00 HST -10:00 + # Time.zone.strptime('22:30:00', '%H:%M:%S') # => Fri, 31 Dec 1999 22:30:00 HST -10:00 + # + # However, if the date component is not provided, but any other upper + # components are supplied, then the day of the month defaults to 1: + # + # Time.zone.strptime('Mar 2000', '%b %Y') # => Wed, 01 Mar 2000 00:00:00 HST -10:00 + def strptime(str, format, now=now()) + parts_to_time(DateTime._strptime(str, format), now) end # Returns an ActiveSupport::TimeWithZone instance representing the current @@ -427,7 +428,36 @@ module ActiveSupport tzinfo.periods_for_local(time) end + def init_with(coder) #:nodoc: + initialize(coder['name']) + end + + def encode_with(coder) #:nodoc: + coder.tag ="!ruby/object:#{self.class}" + coder.map = { 'name' => tzinfo.name } + end + private + def parts_to_time(parts, now) + return if parts.empty? + + time = Time.new( + parts.fetch(:year, now.year), + parts.fetch(:mon, now.month), + parts.fetch(:mday, parts[:year] || parts[:mon] ? 1 : now.day), + parts.fetch(:hour, 0), + parts.fetch(:min, 0), + parts.fetch(:sec, 0) + parts.fetch(:sec_fraction, 0), + parts.fetch(:offset, 0) + ) + + if parts[:offset] + TimeWithZone.new(time.utc, self) + else + TimeWithZone.new(nil, self, time) + end + end + def time_now Time.now end diff --git a/activesupport/test/array_inquirer_test.rb b/activesupport/test/array_inquirer_test.rb new file mode 100644 index 0000000000..b25e5cca86 --- /dev/null +++ b/activesupport/test/array_inquirer_test.rb @@ -0,0 +1,36 @@ +require 'abstract_unit' +require 'active_support/core_ext/array' + +class ArrayInquirerTest < ActiveSupport::TestCase + def setup + @array_inquirer = ActiveSupport::ArrayInquirer.new([:mobile, :tablet]) + end + + def test_individual + assert @array_inquirer.mobile? + assert @array_inquirer.tablet? + assert_not @array_inquirer.desktop? + end + + def test_any + assert @array_inquirer.any?(:mobile, :desktop) + assert @array_inquirer.any?(:watch, :tablet) + assert_not @array_inquirer.any?(:desktop, :watch) + end + + def test_any_with_block + assert @array_inquirer.any? { |v| v == :mobile } + assert_not @array_inquirer.any? { |v| v == :desktop } + end + + def test_respond_to + assert_respond_to @array_inquirer, :development? + end + + def test_inquiry + result = [:mobile, :tablet].inquiry + + assert_instance_of ActiveSupport::ArrayInquirer, result + assert_equal @array_inquirer, result + end +end diff --git a/activesupport/test/caching_test.rb b/activesupport/test/caching_test.rb index 7f5f8feb0d..527538ed9a 100644 --- a/activesupport/test/caching_test.rb +++ b/activesupport/test/caching_test.rb @@ -399,15 +399,16 @@ module CacheStoreBehavior assert_nil @cache.read('foo') end - def test_race_condition_protection - time = Time.now - @cache.write('foo', 'bar', :expires_in => 60) - Time.stubs(:now).returns(time + 61) - result = @cache.fetch('foo', :race_condition_ttl => 10) do - assert_equal 'bar', @cache.read('foo') - "baz" + def test_race_condition_protection_skipped_if_not_defined + @cache.write('foo', 'bar') + time = @cache.send(:read_entry, 'foo', {}).expires_at + Time.stubs(:now).returns(Time.at(time)) + + result = @cache.fetch('foo') do + assert_equal nil, @cache.read('foo') + 'baz' end - assert_equal "baz", result + assert_equal 'baz', result end def test_race_condition_protection_is_limited @@ -437,6 +438,17 @@ module CacheStoreBehavior assert_nil @cache.read('foo') end + def test_race_condition_protection + time = Time.now + @cache.write('foo', 'bar', :expires_in => 60) + Time.stubs(:now).returns(time + 61) + result = @cache.fetch('foo', :race_condition_ttl => 10) do + assert_equal 'bar', @cache.read('foo') + "baz" + end + assert_equal "baz", result + end + def test_crazy_key_characters crazy_key = "#/:*(<+=> )&$%@?;'\"\'`~-" assert @cache.write(crazy_key, "1", :raw => true) @@ -672,6 +684,7 @@ class FileStoreTest < ActiveSupport::TestCase def teardown FileUtils.rm_r(cache_dir) + rescue Errno::ENOENT end def cache_dir @@ -691,6 +704,11 @@ class FileStoreTest < ActiveSupport::TestCase assert File.exist?(filepath) end + def test_clear_without_cache_dir + FileUtils.rm_r(cache_dir) + @cache.clear + end + def test_long_keys @cache.write("a"*10000, 1) assert_equal 1, @cache.read("a"*10000) diff --git a/activesupport/test/callbacks_test.rb b/activesupport/test/callbacks_test.rb index f6abef8cee..cda9732cae 100644 --- a/activesupport/test/callbacks_test.rb +++ b/activesupport/test/callbacks_test.rb @@ -73,8 +73,8 @@ module CallbacksTest class PersonSkipper < Person skip_callback :save, :before, :before_save_method, :if => :yes - skip_callback :save, :after, :before_save_method, :unless => :yes - skip_callback :save, :after, :before_save_method, :if => :no + skip_callback :save, :after, :after_save_method, :unless => :yes + skip_callback :save, :after, :after_save_method, :if => :no skip_callback :save, :before, :before_save_method, :unless => :no skip_callback :save, :before, CallbackClass , :if => :yes def yes; true; end @@ -1021,7 +1021,7 @@ module CallbacksTest define_callbacks :foo n.times { set_callback :foo, :before, callback } def run; run_callbacks :foo; end - def self.skip(thing); skip_callback :foo, :before, thing; end + def self.skip(*things); skip_callback :foo, :before, *things; end } end @@ -1070,11 +1070,11 @@ module CallbacksTest } end - def test_skip_lambda # removes nothing + def test_skip_lambda # raises error calls = [] callback = ->(o) { calls << o } klass = build_class(callback) - 10.times { klass.skip callback } + assert_raises(ArgumentError) { klass.skip callback } klass.new.run assert_equal 10, calls.length end @@ -1088,11 +1088,29 @@ module CallbacksTest assert_equal 0, calls.length end - def test_skip_eval # removes nothing + def test_skip_string # raises error calls = [] klass = build_class("bar") klass.class_eval { define_method(:bar) { calls << klass } } - klass.skip "bar" + assert_raises(ArgumentError) { klass.skip "bar" } + klass.new.run + assert_equal 1, calls.length + end + + def test_skip_undefined_callback # raises error + calls = [] + klass = build_class(:bar) + klass.class_eval { define_method(:bar) { calls << klass } } + assert_raises(ArgumentError) { klass.skip :qux } + klass.new.run + assert_equal 1, calls.length + end + + def test_skip_without_raise # removes nothing + calls = [] + klass = build_class(:bar) + klass.class_eval { define_method(:bar) { calls << klass } } + klass.skip :qux, raise: false klass.new.run assert_equal 1, calls.length end diff --git a/activesupport/test/core_ext/array/conversions_test.rb b/activesupport/test/core_ext/array/conversions_test.rb index 577b889410..507e13f968 100644 --- a/activesupport/test/core_ext/array/conversions_test.rb +++ b/activesupport/test/core_ext/array/conversions_test.rb @@ -60,6 +60,12 @@ class ToSentenceTest < ActiveSupport::TestCase assert_equal exception.message, "Unknown key: :passing. Valid keys are: :words_connector, :two_words_connector, :last_word_connector, :locale" end + + def test_always_returns_string + assert_instance_of String, [ActiveSupport::SafeBuffer.new('one')].to_sentence + assert_instance_of String, [ActiveSupport::SafeBuffer.new('one'), 'two'].to_sentence + assert_instance_of String, [ActiveSupport::SafeBuffer.new('one'), 'two', 'three'].to_sentence + end end class ToSTest < ActiveSupport::TestCase diff --git a/activesupport/test/core_ext/marshal_test.rb b/activesupport/test/core_ext/marshal_test.rb index 8f3f710dfd..e49330128b 100644 --- a/activesupport/test/core_ext/marshal_test.rb +++ b/activesupport/test/core_ext/marshal_test.rb @@ -15,7 +15,7 @@ class MarshalTest < ActiveSupport::TestCase sanity_data = ["test", [1, 2, 3], {a: [1, 2, 3]}, ActiveSupport::TestCase] sanity_data.each do |obj| dumped = Marshal.dump(obj) - assert_equal Marshal.load_without_autoloading(dumped), Marshal.load(dumped) + assert_equal Marshal.method(:load).super_method.call(dumped), Marshal.load(dumped) end end @@ -121,4 +121,4 @@ class MarshalTest < ActiveSupport::TestCase end end end -end
\ No newline at end of file +end diff --git a/activesupport/test/core_ext/module_test.rb b/activesupport/test/core_ext/module_test.rb index c9c9b66a6c..bdfbadcf1d 100644 --- a/activesupport/test/core_ext/module_test.rb +++ b/activesupport/test/core_ext/module_test.rb @@ -358,148 +358,178 @@ class MethodAliasingTest < ActiveSupport::TestCase Object.instance_eval { remove_const :FooClassWithBarMethod } end - def test_alias_method_chain - assert @instance.respond_to?(:bar) - feature_aliases = [:bar_with_baz, :bar_without_baz] + def test_alias_method_chain_deprecated + assert_deprecated(/alias_method_chain/) do + Module.new do + def base + end + + def base_with_deprecated + end - feature_aliases.each do |method| - assert !@instance.respond_to?(method) + alias_method_chain :base, :deprecated + end end + end - assert_equal 'bar', @instance.bar + def test_alias_method_chain + assert_deprecated(/alias_method_chain/) do + assert @instance.respond_to?(:bar) + feature_aliases = [:bar_with_baz, :bar_without_baz] - FooClassWithBarMethod.class_eval { include BarMethodAliaser } + feature_aliases.each do |method| + assert !@instance.respond_to?(method) + end - feature_aliases.each do |method| - assert_respond_to @instance, method - end + assert_equal 'bar', @instance.bar + + FooClassWithBarMethod.class_eval { include BarMethodAliaser } + + feature_aliases.each do |method| + assert_respond_to @instance, method + end - assert_equal 'bar_with_baz', @instance.bar - assert_equal 'bar', @instance.bar_without_baz + assert_equal 'bar_with_baz', @instance.bar + assert_equal 'bar', @instance.bar_without_baz + end end def test_alias_method_chain_with_punctuation_method - FooClassWithBarMethod.class_eval do - def quux!; 'quux' end - end + assert_deprecated(/alias_method_chain/) do + FooClassWithBarMethod.class_eval do + def quux!; 'quux' end + end - assert !@instance.respond_to?(:quux_with_baz!) - FooClassWithBarMethod.class_eval do - include BarMethodAliaser - alias_method_chain :quux!, :baz - end - assert_respond_to @instance, :quux_with_baz! + assert !@instance.respond_to?(:quux_with_baz!) + FooClassWithBarMethod.class_eval do + include BarMethodAliaser + alias_method_chain :quux!, :baz + end + assert_respond_to @instance, :quux_with_baz! - assert_equal 'quux_with_baz', @instance.quux! - assert_equal 'quux', @instance.quux_without_baz! + assert_equal 'quux_with_baz', @instance.quux! + assert_equal 'quux', @instance.quux_without_baz! + end end def test_alias_method_chain_with_same_names_between_predicates_and_bang_methods - FooClassWithBarMethod.class_eval do - def quux!; 'quux!' end - def quux?; true end - def quux=(v); 'quux=' end - end + assert_deprecated(/alias_method_chain/) do + FooClassWithBarMethod.class_eval do + def quux!; 'quux!' end + def quux?; true end + def quux=(v); 'quux=' end + end - assert !@instance.respond_to?(:quux_with_baz!) - assert !@instance.respond_to?(:quux_with_baz?) - assert !@instance.respond_to?(:quux_with_baz=) + assert !@instance.respond_to?(:quux_with_baz!) + assert !@instance.respond_to?(:quux_with_baz?) + assert !@instance.respond_to?(:quux_with_baz=) - FooClassWithBarMethod.class_eval { include BarMethodAliaser } - assert_respond_to @instance, :quux_with_baz! - assert_respond_to @instance, :quux_with_baz? - assert_respond_to @instance, :quux_with_baz= + FooClassWithBarMethod.class_eval { include BarMethodAliaser } + assert_respond_to @instance, :quux_with_baz! + assert_respond_to @instance, :quux_with_baz? + assert_respond_to @instance, :quux_with_baz= - FooClassWithBarMethod.alias_method_chain :quux!, :baz - assert_equal 'quux!_with_baz', @instance.quux! - assert_equal 'quux!', @instance.quux_without_baz! + FooClassWithBarMethod.alias_method_chain :quux!, :baz + assert_equal 'quux!_with_baz', @instance.quux! + assert_equal 'quux!', @instance.quux_without_baz! - FooClassWithBarMethod.alias_method_chain :quux?, :baz - assert_equal false, @instance.quux? - assert_equal true, @instance.quux_without_baz? + FooClassWithBarMethod.alias_method_chain :quux?, :baz + assert_equal false, @instance.quux? + assert_equal true, @instance.quux_without_baz? - FooClassWithBarMethod.alias_method_chain :quux=, :baz - assert_equal 'quux=_with_baz', @instance.send(:quux=, 1234) - assert_equal 'quux=', @instance.send(:quux_without_baz=, 1234) + FooClassWithBarMethod.alias_method_chain :quux=, :baz + assert_equal 'quux=_with_baz', @instance.send(:quux=, 1234) + assert_equal 'quux=', @instance.send(:quux_without_baz=, 1234) + end end def test_alias_method_chain_with_feature_punctuation - FooClassWithBarMethod.class_eval do - def quux; 'quux' end - def quux?; 'quux?' end - include BarMethodAliaser - alias_method_chain :quux, :baz! - end + assert_deprecated(/alias_method_chain/) do + FooClassWithBarMethod.class_eval do + def quux; 'quux' end + def quux?; 'quux?' end + include BarMethodAliaser + alias_method_chain :quux, :baz! + end - assert_nothing_raised do - assert_equal 'quux_with_baz', @instance.quux_with_baz! - end + assert_nothing_raised do + assert_equal 'quux_with_baz', @instance.quux_with_baz! + end - assert_raise(NameError) do - FooClassWithBarMethod.alias_method_chain :quux?, :baz! + assert_raise(NameError) do + FooClassWithBarMethod.alias_method_chain :quux?, :baz! + end end end def test_alias_method_chain_yields_target_and_punctuation - args = nil + assert_deprecated(/alias_method_chain/) do + args = nil - FooClassWithBarMethod.class_eval do - def quux?; end - include BarMethods + FooClassWithBarMethod.class_eval do + def quux?; end + include BarMethods - FooClassWithBarMethod.alias_method_chain :quux?, :baz do |target, punctuation| - args = [target, punctuation] + FooClassWithBarMethod.alias_method_chain :quux?, :baz do |target, punctuation| + args = [target, punctuation] + end end - end - assert_not_nil args - assert_equal 'quux', args[0] - assert_equal '?', args[1] + assert_not_nil args + assert_equal 'quux', args[0] + assert_equal '?', args[1] + end end def test_alias_method_chain_preserves_private_method_status - FooClassWithBarMethod.class_eval do - def duck; 'duck' end - include BarMethodAliaser - private :duck - alias_method_chain :duck, :orange - end + assert_deprecated(/alias_method_chain/) do + FooClassWithBarMethod.class_eval do + def duck; 'duck' end + include BarMethodAliaser + private :duck + alias_method_chain :duck, :orange + end - assert_raise NoMethodError do - @instance.duck - end + assert_raise NoMethodError do + @instance.duck + end - assert_equal 'duck_with_orange', @instance.instance_eval { duck } - assert FooClassWithBarMethod.private_method_defined?(:duck) + assert_equal 'duck_with_orange', @instance.instance_eval { duck } + assert FooClassWithBarMethod.private_method_defined?(:duck) + end end def test_alias_method_chain_preserves_protected_method_status - FooClassWithBarMethod.class_eval do - def duck; 'duck' end - include BarMethodAliaser - protected :duck - alias_method_chain :duck, :orange - end + assert_deprecated(/alias_method_chain/) do + FooClassWithBarMethod.class_eval do + def duck; 'duck' end + include BarMethodAliaser + protected :duck + alias_method_chain :duck, :orange + end - assert_raise NoMethodError do - @instance.duck - end + assert_raise NoMethodError do + @instance.duck + end - assert_equal 'duck_with_orange', @instance.instance_eval { duck } - assert FooClassWithBarMethod.protected_method_defined?(:duck) + assert_equal 'duck_with_orange', @instance.instance_eval { duck } + assert FooClassWithBarMethod.protected_method_defined?(:duck) + end end def test_alias_method_chain_preserves_public_method_status - FooClassWithBarMethod.class_eval do - def duck; 'duck' end - include BarMethodAliaser - public :duck - alias_method_chain :duck, :orange - end + assert_deprecated(/alias_method_chain/) do + FooClassWithBarMethod.class_eval do + def duck; 'duck' end + include BarMethodAliaser + public :duck + alias_method_chain :duck, :orange + end - assert_equal 'duck_with_orange', @instance.duck - assert FooClassWithBarMethod.public_method_defined?(:duck) + assert_equal 'duck_with_orange', @instance.duck + assert FooClassWithBarMethod.public_method_defined?(:duck) + end end def test_delegate_with_case diff --git a/activesupport/test/core_ext/securerandom.rb b/activesupport/test/core_ext/secure_random_test.rb index dfacb7fe9f..dfacb7fe9f 100644 --- a/activesupport/test/core_ext/securerandom.rb +++ b/activesupport/test/core_ext/secure_random_test.rb diff --git a/activesupport/test/core_ext/time_with_zone_test.rb b/activesupport/test/core_ext/time_with_zone_test.rb index 0f5522b712..79d78c02cd 100644 --- a/activesupport/test/core_ext/time_with_zone_test.rb +++ b/activesupport/test/core_ext/time_with_zone_test.rb @@ -1,6 +1,7 @@ require 'abstract_unit' require 'active_support/time' require 'time_zone_test_helpers' +require 'active_support/core_ext/string/strip' class TimeWithZoneTest < ActiveSupport::TestCase include TimeZoneTestHelpers @@ -123,11 +124,53 @@ class TimeWithZoneTest < ActiveSupport::TestCase end def test_to_yaml - assert_match(/^--- 2000-01-01 00:00:00(\.0+)?\s*Z\n/, @twz.to_yaml) + yaml = <<-EOF.strip_heredoc + --- !ruby/object:ActiveSupport::TimeWithZone + utc: 2000-01-01 00:00:00.000000000 Z + zone: !ruby/object:ActiveSupport::TimeZone + name: America/New_York + time: 1999-12-31 19:00:00.000000000 Z + EOF + + assert_equal(yaml, @twz.to_yaml) end def test_ruby_to_yaml - assert_match(/---\s*\n:twz: 2000-01-01 00:00:00(\.0+)?\s*Z\n/, {:twz => @twz}.to_yaml) + yaml = <<-EOF.strip_heredoc + --- + twz: !ruby/object:ActiveSupport::TimeWithZone + utc: 2000-01-01 00:00:00.000000000 Z + zone: !ruby/object:ActiveSupport::TimeZone + name: America/New_York + time: 1999-12-31 19:00:00.000000000 Z + EOF + + assert_equal(yaml, { 'twz' => @twz }.to_yaml) + end + + def test_yaml_load + yaml = <<-EOF.strip_heredoc + --- !ruby/object:ActiveSupport::TimeWithZone + utc: 2000-01-01 00:00:00.000000000 Z + zone: !ruby/object:ActiveSupport::TimeZone + name: America/New_York + time: 1999-12-31 19:00:00.000000000 Z + EOF + + assert_equal(@twz, YAML.load(yaml)) + end + + def test_ruby_yaml_load + yaml = <<-EOF.strip_heredoc + --- + twz: !ruby/object:ActiveSupport::TimeWithZone + utc: 2000-01-01 00:00:00.000000000 Z + zone: !ruby/object:ActiveSupport::TimeZone + name: America/New_York + time: 1999-12-31 19:00:00.000000000 Z + EOF + + assert_equal({ 'twz' => @twz }, YAML.load(yaml)) end def test_httpdate @@ -867,13 +910,6 @@ class TimeWithZoneMethodsForTimeAndDateTimeTest < ActiveSupport::TestCase end end - def test_in_time_zone_with_dst - travel_to(Time.utc(2014, 5, 20, 4, 59, 59)) - time = Time.now.in_time_zone(-4) - assert_equal (-4*3600), time.time_zone.utc_total_offset - travel_back - end - def test_in_time_zone_with_invalid_argument assert_raise(ArgumentError) { @t.in_time_zone("No such timezone exists") } assert_raise(ArgumentError) { @dt.in_time_zone("No such timezone exists") } diff --git a/activesupport/test/dependencies_test.rb b/activesupport/test/dependencies_test.rb index 702e26859a..4a1d90bfd6 100644 --- a/activesupport/test/dependencies_test.rb +++ b/activesupport/test/dependencies_test.rb @@ -160,7 +160,7 @@ class DependenciesTest < ActiveSupport::TestCase def test_ensures_the_expected_constant_is_defined with_autoloading_fixtures do e = assert_raise(LoadError) { Typo } - assert_match %r{Unable to autoload constant Typo, expected .*activesupport/test/autoloading_fixtures/typo.rb to define it}, e.message + assert_match %r{Unable to autoload constant Typo, expected .*/test/autoloading_fixtures/typo.rb to define it}, e.message end end @@ -178,7 +178,7 @@ class DependenciesTest < ActiveSupport::TestCase assert_equal 1, TypO e = assert_raise(LoadError) { Typo } - assert_match %r{Unable to autoload constant Typo, expected .*activesupport/test/autoloading_fixtures/typo.rb to define it}, e.message + assert_match %r{Unable to autoload constant Typo, expected .*/test/autoloading_fixtures/typo.rb to define it}, e.message end end diff --git a/activesupport/test/test_case_test.rb b/activesupport/test/test_case_test.rb index 151b623171..9e6d1a91d0 100644 --- a/activesupport/test/test_case_test.rb +++ b/activesupport/test/test_case_test.rb @@ -205,15 +205,4 @@ class TestOrderTest < ActiveSupport::TestCase assert_equal :random, self.class.test_order assert_equal :random, Class.new(ActiveSupport::TestCase).test_order end - - def test_i_suck_and_my_tests_are_order_dependent! - ActiveSupport::TestCase.test_order = :random - - klass = Class.new(ActiveSupport::TestCase) do - i_suck_and_my_tests_are_order_dependent! - end - - assert_equal :alpha, klass.test_order - assert_equal :random, ActiveSupport::TestCase.test_order - end end diff --git a/activesupport/test/time_zone_test.rb b/activesupport/test/time_zone_test.rb index 7888b9919b..5e0474f449 100644 --- a/activesupport/test/time_zone_test.rb +++ b/activesupport/test/time_zone_test.rb @@ -311,6 +311,80 @@ class TimeZoneTest < ActiveSupport::TestCase end end + def test_strptime + zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)'] + twz = zone.strptime('1999-12-31 12:00:00', '%Y-%m-%d %H:%M:%S') + assert_equal Time.utc(1999,12,31,17), twz + assert_equal Time.utc(1999,12,31,12), twz.time + assert_equal Time.utc(1999,12,31,17), twz.utc + assert_equal zone, twz.time_zone + end + + def test_strptime_with_nondefault_time_zone + with_tz_default ActiveSupport::TimeZone['Pacific Time (US & Canada)'] do + zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)'] + twz = zone.strptime('1999-12-31 12:00:00', '%Y-%m-%d %H:%M:%S') + assert_equal Time.utc(1999,12,31,17), twz + assert_equal Time.utc(1999,12,31,12), twz.time + assert_equal Time.utc(1999,12,31,17), twz.utc + assert_equal zone, twz.time_zone + end + end + + def test_strptime_with_explicit_time_zone_as_abbrev + zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)'] + twz = zone.strptime('1999-12-31 12:00:00 PST', '%Y-%m-%d %H:%M:%S %Z') + assert_equal Time.utc(1999,12,31,20), twz + assert_equal Time.utc(1999,12,31,15), twz.time + assert_equal Time.utc(1999,12,31,20), twz.utc + assert_equal zone, twz.time_zone + end + + def test_strptime_with_explicit_time_zone_as_h_offset + zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)'] + twz = zone.strptime('1999-12-31 12:00:00 -08', '%Y-%m-%d %H:%M:%S %:::z') + assert_equal Time.utc(1999,12,31,20), twz + assert_equal Time.utc(1999,12,31,15), twz.time + assert_equal Time.utc(1999,12,31,20), twz.utc + assert_equal zone, twz.time_zone + end + + def test_strptime_with_explicit_time_zone_as_hm_offset + zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)'] + twz = zone.strptime('1999-12-31 12:00:00 -08:00', '%Y-%m-%d %H:%M:%S %:z') + assert_equal Time.utc(1999,12,31,20), twz + assert_equal Time.utc(1999,12,31,15), twz.time + assert_equal Time.utc(1999,12,31,20), twz.utc + assert_equal zone, twz.time_zone + end + + def test_strptime_with_explicit_time_zone_as_hms_offset + zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)'] + twz = zone.strptime('1999-12-31 12:00:00 -08:00:00', '%Y-%m-%d %H:%M:%S %::z') + assert_equal Time.utc(1999,12,31,20), twz + assert_equal Time.utc(1999,12,31,15), twz.time + assert_equal Time.utc(1999,12,31,20), twz.utc + assert_equal zone, twz.time_zone + end + + def test_strptime_with_almost_explicit_time_zone + zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)'] + twz = zone.strptime('1999-12-31 12:00:00 %Z', '%Y-%m-%d %H:%M:%S %%Z') + assert_equal Time.utc(1999,12,31,17), twz + assert_equal Time.utc(1999,12,31,12), twz.time + assert_equal Time.utc(1999,12,31,17), twz.utc + assert_equal zone, twz.time_zone + end + + def test_strptime_with_day_omitted + with_env_tz 'US/Eastern' do + zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)'] + assert_equal Time.local(2000, 2, 1), zone.strptime('Feb', '%b', Time.local(2000, 1, 1)) + assert_equal Time.local(2005, 2, 1), zone.strptime('Feb 2005', '%b %Y', Time.local(2000, 1, 1)) + assert_equal Time.local(2005, 2, 2), zone.strptime('2 Feb 2005', '%e %b %Y', Time.local(2000, 1, 1)) + end + end + def test_utc_offset_lazy_loaded_from_tzinfo_when_not_passed_in_to_initialize tzinfo = TZInfo::Timezone.get('America/New_York') zone = ActiveSupport::TimeZone.create(tzinfo.name, nil, tzinfo) @@ -413,4 +487,13 @@ class TimeZoneTest < ActiveSupport::TestCase assert ActiveSupport::TimeZone.us_zones.include?(ActiveSupport::TimeZone["Hawaii"]) assert !ActiveSupport::TimeZone.us_zones.include?(ActiveSupport::TimeZone["Kuala Lumpur"]) end + + def test_to_yaml + assert_equal("--- !ruby/object:ActiveSupport::TimeZone\nname: Pacific/Honolulu\n", ActiveSupport::TimeZone["Hawaii"].to_yaml) + assert_equal("--- !ruby/object:ActiveSupport::TimeZone\nname: Europe/London\n", ActiveSupport::TimeZone["Europe/London"].to_yaml) + end + + def test_yaml_load + assert_equal(ActiveSupport::TimeZone["Pacific/Honolulu"], YAML.load("--- !ruby/object:ActiveSupport::TimeZone\nname: Pacific/Honolulu\n")) + end end diff --git a/guides/CHANGELOG.md b/guides/CHANGELOG.md index dd5ca4b395..fd177b4238 100644 --- a/guides/CHANGELOG.md +++ b/guides/CHANGELOG.md @@ -1,3 +1,7 @@ +* New section in Configuring: Configuring Active Job + + *Eliot Sykes* + * New section in Active Record Association Basics: Single Table Inheritance *Andrey Nering* diff --git a/guides/bug_report_templates/generic_gem.rb b/guides/bug_report_templates/generic_gem.rb new file mode 100644 index 0000000000..3e3f8593d4 --- /dev/null +++ b/guides/bug_report_templates/generic_gem.rb @@ -0,0 +1,15 @@ +# Activate the gems you are reporting the issue against. +gem 'activesupport', '4.2.0' +require 'active_support' +require 'active_support/core_ext/object/blank' +require 'minitest/autorun' + +# Ensure backward compatibility with Minitest 4 +Minitest::Test = MiniTest::Unit::TestCase unless defined?(Minitest::Test) + +class BugTest < Minitest::Test + def test_stuff + assert "zomg".present? + refute "".present? + end +end diff --git a/guides/bug_report_templates/generic_master.rb b/guides/bug_report_templates/generic_master.rb new file mode 100644 index 0000000000..d930482d4e --- /dev/null +++ b/guides/bug_report_templates/generic_master.rb @@ -0,0 +1,26 @@ +unless File.exist?('Gemfile') + File.write('Gemfile', <<-GEMFILE) + source 'https://rubygems.org' + gem 'rails', github: 'rails/rails' + gem 'arel', github: 'rails/arel' + GEMFILE + + system 'bundle' +end + +require 'bundler' +Bundler.setup(:default) + +require 'active_support' +require 'active_support/core_ext/object/blank' +require 'minitest/autorun' + +# Ensure backward compatibility with Minitest 4 +Minitest::Test = MiniTest::Unit::TestCase unless defined?(Minitest::Test) + +class BugTest < Minitest::Test + def test_stuff + assert "zomg".present? + refute "".present? + end +end diff --git a/guides/rails_guides.rb b/guides/rails_guides.rb index 762ab1c0e2..367ed0b12e 100644 --- a/guides/rails_guides.rb +++ b/guides/rails_guides.rb @@ -1,13 +1,6 @@ pwd = File.dirname(__FILE__) $:.unshift pwd -# This is a predicate useful for the doc:guides task of applications. -def bundler? - # Note that rake sets the cwd to the one that contains the Rakefile - # being executed. - File.exist?('Gemfile') -end - begin # Guides generation in the Rails repo. as_lib = File.join(pwd, "../activesupport/lib") @@ -20,44 +13,5 @@ rescue LoadError gem "actionpack", '>= 3.0' end -begin - require 'redcarpet' -rescue LoadError - # This can happen if doc:guides is executed in an application. - $stderr.puts('Generating guides requires Redcarpet 3.2.2+.') - $stderr.puts(<<ERROR) if bundler? -Please add - - gem 'redcarpet', '~> 3.2.2' - -to the Gemfile, run - - bundle install - -and try again. -ERROR - exit 1 -end - -begin - require 'nokogiri' -rescue LoadError - # This can happen if doc:guides is executed in an application. - $stderr.puts('Generating guides requires Nokogiri.') - $stderr.puts(<<ERROR) if bundler? -Please add - - gem 'nokogiri' - -to the Gemfile, run - - bundle install - -and try again. -ERROR - exit 1 -end - -require 'rails_guides/markdown' require "rails_guides/generator" RailsGuides::Generator.new.generate diff --git a/guides/rails_guides/generator.rb b/guides/rails_guides/generator.rb index aa900454c8..43f6f7eecf 100644 --- a/guides/rails_guides/generator.rb +++ b/guides/rails_guides/generator.rb @@ -57,6 +57,7 @@ require 'active_support/core_ext/object/blank' require 'action_controller' require 'action_view' +require 'rails_guides/markdown' require 'rails_guides/indexer' require 'rails_guides/helpers' require 'rails_guides/levenshtein' diff --git a/guides/rails_guides/levenshtein.rb b/guides/rails_guides/levenshtein.rb index 36183fd321..049f633258 100644 --- a/guides/rails_guides/levenshtein.rb +++ b/guides/rails_guides/levenshtein.rb @@ -14,10 +14,13 @@ module RailsGuides d = (0..m).to_a x = nil - str1.each_char.each_with_index do |char1,i| + # avoid duplicating an enumerable object in the loop + str2_codepoint_enumerable = str2.each_codepoint + + str1.each_codepoint.with_index do |char1, i| e = i+1 - str2.each_char.each_with_index do |char2,j| + str2_codepoint_enumerable.with_index do |char2, j| cost = (char1 == char2) ? 0 : 1 x = [ d[j+1] + 1, # insertion diff --git a/guides/source/2_3_release_notes.md b/guides/source/2_3_release_notes.md index 328656f4a4..0a62f34371 100644 --- a/guides/source/2_3_release_notes.md +++ b/guides/source/2_3_release_notes.md @@ -187,7 +187,7 @@ MySQL supports a reconnect flag in its connections - if set to true, then the cl * Lead Contributor: [Dov Murik](http://twitter.com/dubek) * More information: - * [Controlling Automatic Reconnection Behavior](http://dev.mysql.com/doc/refman/5.0/en/auto-reconnect.html) + * [Controlling Automatic Reconnection Behavior](http://dev.mysql.com/doc/refman/5.6/en/auto-reconnect.html) * [MySQL auto-reconnect revisited](http://groups.google.com/group/rubyonrails-core/browse_thread/thread/49d2a7e9c96cb9f4) ### Other Active Record Changes diff --git a/guides/source/3_1_release_notes.md b/guides/source/3_1_release_notes.md index e187e5f9ab..537aa5a371 100644 --- a/guides/source/3_1_release_notes.md +++ b/guides/source/3_1_release_notes.md @@ -174,7 +174,7 @@ Rails Architectural Changes The major change in Rails 3.1 is the Assets Pipeline. It makes CSS and JavaScript first-class code citizens and enables proper organization, including use in plugins and engines. -The assets pipeline is powered by [Sprockets](https://github.com/sstephenson/sprockets) and is covered in the [Asset Pipeline](asset_pipeline.html) guide. +The assets pipeline is powered by [Sprockets](https://github.com/rails/sprockets) and is covered in the [Asset Pipeline](asset_pipeline.html) guide. ### HTTP Streaming diff --git a/guides/source/4_0_release_notes.md b/guides/source/4_0_release_notes.md index bd35e2d31a..9feaff098a 100644 --- a/guides/source/4_0_release_notes.md +++ b/guides/source/4_0_release_notes.md @@ -59,25 +59,25 @@ Major Features ### Upgrade - * **Ruby 1.9.3** ([commit](https://github.com/rails/rails/commit/a0380e808d3dbd2462df17f5d3b7fcd8bd812496)) - Ruby 2.0 preferred; 1.9.3+ required - * **[New deprecation policy](http://www.youtube.com/watch?v=z6YgD6tVPQs)** - Deprecated features are warnings in Rails 4.0 and will be removed in Rails 4.1. - * **ActionPack page and action caching** ([commit](https://github.com/rails/rails/commit/b0a7068564f0c95e7ef28fc39d0335ed17d93e90)) - Page and action caching are extracted to a separate gem. Page and action caching requires too much manual intervention (manually expiring caches when the underlying model objects are updated). Instead, use Russian doll caching. - * **ActiveRecord observers** ([commit](https://github.com/rails/rails/commit/ccecab3ba950a288b61a516bf9b6962e384aae0b)) - Observers are extracted to a separate gem. Observers are only needed for page and action caching, and can lead to spaghetti code. - * **ActiveRecord session store** ([commit](https://github.com/rails/rails/commit/0ffe19056c8e8b2f9ae9d487b896cad2ce9387ad)) - The ActiveRecord session store is extracted to a separate gem. Storing sessions in SQL is costly. Instead, use cookie sessions, memcache sessions, or a custom session store. - * **ActiveModel mass assignment protection** ([commit](https://github.com/rails/rails/commit/f8c9a4d3e88181cee644f91e1342bfe896ca64c6)) - Rails 3 mass assignment protection is deprecated. Instead, use strong parameters. - * **ActiveResource** ([commit](https://github.com/rails/rails/commit/f1637bf2bb00490203503fbd943b73406e043d1d)) - ActiveResource is extracted to a separate gem. ActiveResource was not widely used. - * **vendor/plugins removed** ([commit](https://github.com/rails/rails/commit/853de2bd9ac572735fa6cf59fcf827e485a231c3)) - Use a Gemfile to manage installed gems. +* **Ruby 1.9.3** ([commit](https://github.com/rails/rails/commit/a0380e808d3dbd2462df17f5d3b7fcd8bd812496)) - Ruby 2.0 preferred; 1.9.3+ required +* **[New deprecation policy](http://www.youtube.com/watch?v=z6YgD6tVPQs)** - Deprecated features are warnings in Rails 4.0 and will be removed in Rails 4.1. +* **ActionPack page and action caching** ([commit](https://github.com/rails/rails/commit/b0a7068564f0c95e7ef28fc39d0335ed17d93e90)) - Page and action caching are extracted to a separate gem. Page and action caching requires too much manual intervention (manually expiring caches when the underlying model objects are updated). Instead, use Russian doll caching. +* **ActiveRecord observers** ([commit](https://github.com/rails/rails/commit/ccecab3ba950a288b61a516bf9b6962e384aae0b)) - Observers are extracted to a separate gem. Observers are only needed for page and action caching, and can lead to spaghetti code. +* **ActiveRecord session store** ([commit](https://github.com/rails/rails/commit/0ffe19056c8e8b2f9ae9d487b896cad2ce9387ad)) - The ActiveRecord session store is extracted to a separate gem. Storing sessions in SQL is costly. Instead, use cookie sessions, memcache sessions, or a custom session store. +* **ActiveModel mass assignment protection** ([commit](https://github.com/rails/rails/commit/f8c9a4d3e88181cee644f91e1342bfe896ca64c6)) - Rails 3 mass assignment protection is deprecated. Instead, use strong parameters. +* **ActiveResource** ([commit](https://github.com/rails/rails/commit/f1637bf2bb00490203503fbd943b73406e043d1d)) - ActiveResource is extracted to a separate gem. ActiveResource was not widely used. +* **vendor/plugins removed** ([commit](https://github.com/rails/rails/commit/853de2bd9ac572735fa6cf59fcf827e485a231c3)) - Use a Gemfile to manage installed gems. ### ActionPack - * **Strong parameters** ([commit](https://github.com/rails/rails/commit/a8f6d5c6450a7fe058348a7f10a908352bb6c7fc)) - Only allow whitelisted parameters to update model objects (`params.permit(:title, :text)`). - * **Routing concerns** ([commit](https://github.com/rails/rails/commit/0dd24728a088fcb4ae616bb5d62734aca5276b1b)) - In the routing DSL, factor out common subroutes (`comments` from `/posts/1/comments` and `/videos/1/comments`). - * **ActionController::Live** ([commit](https://github.com/rails/rails/commit/af0a9f9eefaee3a8120cfd8d05cbc431af376da3)) - Stream JSON with `response.stream`. - * **Declarative ETags** ([commit](https://github.com/rails/rails/commit/ed5c938fa36995f06d4917d9543ba78ed506bb8d)) - Add controller-level etag additions that will be part of the action etag computation. - * **[Russian doll caching](http://37signals.com/svn/posts/3113-how-key-based-cache-expiration-works)** ([commit](https://github.com/rails/rails/commit/4154bf012d2bec2aae79e4a49aa94a70d3e91d49)) - Cache nested fragments of views. Each fragment expires based on a set of dependencies (a cache key). The cache key is usually a template version number and a model object. - * **Turbolinks** ([commit](https://github.com/rails/rails/commit/e35d8b18d0649c0ecc58f6b73df6b3c8d0c6bb74)) - Serve only one initial HTML page. When the user navigates to another page, use pushState to update the URL and use AJAX to update the title and body. - * **Decouple ActionView from ActionController** ([commit](https://github.com/rails/rails/commit/78b0934dd1bb84e8f093fb8ef95ca99b297b51cd)) - ActionView was decoupled from ActionPack and will be moved to a separated gem in Rails 4.1. - * **Do not depend on ActiveModel** ([commit](https://github.com/rails/rails/commit/166dbaa7526a96fdf046f093f25b0a134b277a68)) - ActionPack no longer depends on ActiveModel. +* **Strong parameters** ([commit](https://github.com/rails/rails/commit/a8f6d5c6450a7fe058348a7f10a908352bb6c7fc)) - Only allow whitelisted parameters to update model objects (`params.permit(:title, :text)`). +* **Routing concerns** ([commit](https://github.com/rails/rails/commit/0dd24728a088fcb4ae616bb5d62734aca5276b1b)) - In the routing DSL, factor out common subroutes (`comments` from `/posts/1/comments` and `/videos/1/comments`). +* **ActionController::Live** ([commit](https://github.com/rails/rails/commit/af0a9f9eefaee3a8120cfd8d05cbc431af376da3)) - Stream JSON with `response.stream`. +* **Declarative ETags** ([commit](https://github.com/rails/rails/commit/ed5c938fa36995f06d4917d9543ba78ed506bb8d)) - Add controller-level etag additions that will be part of the action etag computation. +* **[Russian doll caching](http://37signals.com/svn/posts/3113-how-key-based-cache-expiration-works)** ([commit](https://github.com/rails/rails/commit/4154bf012d2bec2aae79e4a49aa94a70d3e91d49)) - Cache nested fragments of views. Each fragment expires based on a set of dependencies (a cache key). The cache key is usually a template version number and a model object. +* **Turbolinks** ([commit](https://github.com/rails/rails/commit/e35d8b18d0649c0ecc58f6b73df6b3c8d0c6bb74)) - Serve only one initial HTML page. When the user navigates to another page, use pushState to update the URL and use AJAX to update the title and body. +* **Decouple ActionView from ActionController** ([commit](https://github.com/rails/rails/commit/78b0934dd1bb84e8f093fb8ef95ca99b297b51cd)) - ActionView was decoupled from ActionPack and will be moved to a separated gem in Rails 4.1. +* **Do not depend on ActiveModel** ([commit](https://github.com/rails/rails/commit/166dbaa7526a96fdf046f093f25b0a134b277a68)) - ActionPack no longer depends on ActiveModel. ### General @@ -87,14 +87,17 @@ Major Features * **Support for specifying transaction isolation level** ([commit](https://github.com/rails/rails/commit/392eeecc11a291e406db927a18b75f41b2658253)) - Choose whether repeatable reads or improved performance (less locking) is more important. * **Dalli** ([commit](https://github.com/rails/rails/commit/82663306f428a5bbc90c511458432afb26d2f238)) - Use Dalli memcache client for the memcache store. * **Notifications start & finish** ([commit](https://github.com/rails/rails/commit/f08f8750a512f741acb004d0cebe210c5f949f28)) - Active Support instrumentation reports start and finish notifications to subscribers. - * **Thread safe by default** ([commit](https://github.com/rails/rails/commit/5d416b907864d99af55ebaa400fff217e17570cd)) - Rails can run in threaded app servers without additional configuration. Note: Check that the gems you are using are threadsafe. + * **Thread safe by default** ([commit](https://github.com/rails/rails/commit/5d416b907864d99af55ebaa400fff217e17570cd)) - Rails can run in threaded app servers without additional configuration. + +NOTE: Check that the gems you are using are threadsafe. + * **PATCH verb** ([commit](https://github.com/rails/rails/commit/eed9f2539e3ab5a68e798802f464b8e4e95e619e)) - In Rails, PATCH replaces PUT. PATCH is used for partial updates of resources. ### Security - * **match do not catch all** ([commit](https://github.com/rails/rails/commit/90d2802b71a6e89aedfe40564a37bd35f777e541)) - In the routing DSL, match requires the HTTP verb or verbs to be specified. - * **html entities escaped by default** ([commit](https://github.com/rails/rails/commit/5f189f41258b83d49012ec5a0678d827327e7543)) - Strings rendered in erb are escaped unless wrapped with `raw` or `html_safe` is called. - * **New security headers** ([commit](https://github.com/rails/rails/commit/6794e92b204572d75a07bd6413bdae6ae22d5a82)) - Rails sends the following headers with every HTTP request: `X-Frame-Options` (prevents clickjacking by forbidding the browser from embedding the page in a frame), `X-XSS-Protection` (asks the browser to halt script injection) and `X-Content-Type-Options` (prevents the browser from opening a jpeg as an exe). +* **match do not catch all** ([commit](https://github.com/rails/rails/commit/90d2802b71a6e89aedfe40564a37bd35f777e541)) - In the routing DSL, match requires the HTTP verb or verbs to be specified. +* **html entities escaped by default** ([commit](https://github.com/rails/rails/commit/5f189f41258b83d49012ec5a0678d827327e7543)) - Strings rendered in erb are escaped unless wrapped with `raw` or `html_safe` is called. +* **New security headers** ([commit](https://github.com/rails/rails/commit/6794e92b204572d75a07bd6413bdae6ae22d5a82)) - Rails sends the following headers with every HTTP request: `X-Frame-Options` (prevents clickjacking by forbidding the browser from embedding the page in a frame), `X-XSS-Protection` (asks the browser to halt script injection) and `X-Content-Type-Options` (prevents the browser from opening a jpeg as an exe). Extraction of features to gems --------------------------- diff --git a/guides/source/4_2_release_notes.md b/guides/source/4_2_release_notes.md index 366d9d26b4..d5b3766a5b 100644 --- a/guides/source/4_2_release_notes.md +++ b/guides/source/4_2_release_notes.md @@ -214,9 +214,8 @@ end Due to a [change in Rack](https://github.com/rack/rack/commit/28b014484a8ac0bbb388e7eaeeef159598ec64fc), `rails server` now listens on `localhost` instead of `0.0.0.0` by default. This -should have minimal impact on the standard development workflow as both -http://127.0.0.1:3000 and http://localhost:3000 will continue to work as before -on your own machine. +should have minimal impact on the standard development workflow as http://localhost:3000 +will continue to work as before on your own machine. However, with this change you will no longer be able to access the Rails server from a different machine, for example if your development environment @@ -257,7 +256,7 @@ application is using any of these spellings, you will need to update them: * Values in attribute selectors may need to be quoted if they contain non-alphanumeric characters. - ``` + ```ruby # before a[href=/] a[href$=/] @@ -272,7 +271,7 @@ application is using any of these spellings, you will need to update them: For example: - ``` ruby + ```ruby # content: <div><i><p></i></div> # before: @@ -290,7 +289,7 @@ application is using any of these spellings, you will need to update them: used to be raw (e.g. `AT&T`), and now is evaluated (e.g. `AT&T`). - ``` ruby + ```ruby # content: <p>AT&T</p> # before: @@ -302,6 +301,30 @@ application is using any of these spellings, you will need to update them: assert_select('p', 'AT&T') # => false ``` +Furthermore substitutions have changed syntax. + +Now you have to use a `:match` CSS-like selector: + +```ruby +assert_select ":match('id', ?)", 'comment_1' +``` + +Additionally Regexp substitutions look different when the assertion fails. +Notice how `/hello/` here: + +```ruby +assert_select(":match('id', ?)", /hello/) +``` + +becomes `"(?-mix:hello)"`: + +``` +Expected at least 1 element matching "div:match('id', "(?-mix:hello)")", found 0.. +Expected 0 to be >= 1. +``` + +See the [Rails Dom Testing](https://github.com/rails/rails-dom-testing/tree/8798b9349fb9540ad8cb9a0ce6cb88d1384a210b) documentation for more on `assert_select`. + Railties -------- diff --git a/guides/source/action_controller_overview.md b/guides/source/action_controller_overview.md index f68179841e..fab0e20aba 100644 --- a/guides/source/action_controller_overview.md +++ b/guides/source/action_controller_overview.md @@ -9,7 +9,7 @@ After reading this guide, you will know: * How to follow the flow of a request through a controller. * How to restrict parameters passed to your controller. -* Why and how to store data in the session or cookies. +* How and why to store data in the session or cookies. * How to work with filters to execute code during request processing. * How to use Action Controller's built-in HTTP authentication. * How to stream data directly to the user's browser. @@ -21,11 +21,11 @@ After reading this guide, you will know: What Does a Controller Do? -------------------------- -Action Controller is the C in MVC. After routing has determined which controller to use for a request, your controller is responsible for making sense of the request and producing the appropriate output. Luckily, Action Controller does most of the groundwork for you and uses smart conventions to make this as straightforward as possible. +Action Controller is the C in MVC. After routing has determined which controller to use for a request, the controller is responsible for making sense of the request and producing the appropriate output. Luckily, Action Controller does most of the groundwork for you and uses smart conventions to make this as straightforward as possible. For most conventional [RESTful](http://en.wikipedia.org/wiki/Representational_state_transfer) applications, the controller will receive the request (this is invisible to you as the developer), fetch or save data from a model and use a view to create HTML output. If your controller needs to do things a little differently, that's not a problem, this is just the most common way for a controller to work. -A controller can thus be thought of as a middle man between models and views. It makes the model data available to the view so it can display that data to the user, and it saves or updates data from the user to the model. +A controller can thus be thought of as a middleman between models and views. It makes the model data available to the view so it can display that data to the user, and it saves or updates user data to the model. NOTE: For more details on the routing process, see [Rails Routing from the Outside In](routing.html). @@ -34,7 +34,7 @@ Controller Naming Convention The naming convention of controllers in Rails favors pluralization of the last word in the controller's name, although it is not strictly required (e.g. `ApplicationController`). For example, `ClientsController` is preferable to `ClientController`, `SiteAdminsController` is preferable to `SiteAdminController` or `SitesAdminsController`, and so on. -Following this convention will allow you to use the default route generators (e.g. `resources`, etc) without needing to qualify each `:path` or `:controller`, and keeps URL and path helpers' usage consistent throughout your application. See [Layouts & Rendering Guide](layouts_and_rendering.html) for more details. +Following this convention will allow you to use the default route generators (e.g. `resources`, etc) without needing to qualify each `:path` or `:controller`, and will keep URL and path helpers' usage consistent throughout your application. See [Layouts & Rendering Guide](layouts_and_rendering.html) for more details. NOTE: The controller naming convention differs from the naming convention of models, which are expected to be named in singular form. @@ -51,7 +51,7 @@ class ClientsController < ApplicationController end ``` -As an example, if a user goes to `/clients/new` in your application to add a new client, Rails will create an instance of `ClientsController` and run the `new` method. Note that the empty method from the example above would work just fine because Rails will by default render the `new.html.erb` view unless the action says otherwise. The `new` method could make available to the view a `@client` instance variable by creating a new `Client`: +As an example, if a user goes to `/clients/new` in your application to add a new client, Rails will create an instance of `ClientsController` and call its `new` method. Note that the empty method from the example above would work just fine because Rails will by default render the `new.html.erb` view unless the action says otherwise. The `new` method could make available to the view a `@client` instance variable by creating a new `Client`: ```ruby def new @@ -63,7 +63,7 @@ The [Layouts & Rendering Guide](layouts_and_rendering.html) explains this in mor `ApplicationController` inherits from `ActionController::Base`, which defines a number of helpful methods. This guide will cover some of these, but if you're curious to see what's in there, you can see all of them in the API documentation or in the source itself. -Only public methods are callable as actions. It is a best practice to lower the visibility of methods which are not intended to be actions, like auxiliary methods or filters. +Only public methods are callable as actions. It is a best practice to lower the visibility of methods (with `private` or `protected`) which are not intended to be actions, like auxiliary methods or filters. Parameters ---------- @@ -104,13 +104,13 @@ end ### Hash and Array Parameters -The `params` hash is not limited to one-dimensional keys and values. It can contain arrays and (nested) hashes. To send an array of values, append an empty pair of square brackets "[]" to the key name: +The `params` hash is not limited to one-dimensional keys and values. It can contain nested arrays and hashes. To send an array of values, append an empty pair of square brackets "[]" to the key name: ``` GET /clients?ids[]=1&ids[]=2&ids[]=3 ``` -NOTE: The actual URL in this example will be encoded as "/clients?ids%5b%5d=1&ids%5b%5d=2&ids%5b%5d=3" as "[" and "]" are not allowed in URLs. Most of the time you don't have to worry about this because the browser will take care of it for you, and Rails will decode it back when it receives it, but if you ever find yourself having to send those requests to the server manually you have to keep this in mind. +NOTE: The actual URL in this example will be encoded as "/clients?ids%5b%5d=1&ids%5b%5d=2&ids%5b%5d=3" as the "[" and "]" characters are not allowed in URLs. Most of the time you don't have to worry about this because the browser will encode it for you, and Rails will decode it automatically, but if you ever find yourself having to send those requests to the server manually you should keep this in mind. The value of `params[:ids]` will now be `["1", "2", "3"]`. Note that parameter values are always strings; Rails makes no attempt to guess or cast the type. @@ -118,7 +118,7 @@ NOTE: Values such as `[nil]` or `[nil, nil, ...]` in `params` are replaced with `[]` for security reasons by default. See [Security Guide](security.html#unsafe-query-generation) for more information. -To send a hash you include the key name inside the brackets: +To send a hash, you include the key name inside the brackets: ```html <form accept-charset="UTF-8" action="/clients" method="post"> @@ -131,11 +131,11 @@ To send a hash you include the key name inside the brackets: When this form is submitted, the value of `params[:client]` will be `{ "name" => "Acme", "phone" => "12345", "address" => { "postcode" => "12345", "city" => "Carrot City" } }`. Note the nested hash in `params[:client][:address]`. -Note that the `params` hash is actually an instance of `ActiveSupport::HashWithIndifferentAccess`, which acts like a hash but lets you use symbols and strings interchangeably as keys. +The `params` object acts like a Hash, but lets you use symbols and strings interchangeably as keys. ### JSON parameters -If you're writing a web service application, you might find yourself more comfortable accepting parameters in JSON format. If the "Content-Type" header of your request is set to "application/json", Rails will automatically convert your parameters into the `params` hash, which you can access as you would normally. +If you're writing a web service application, you might find yourself more comfortable accepting parameters in JSON format. If the "Content-Type" header of your request is set to "application/json", Rails will automatically load your parameters into the `params` hash, which you can access as you would normally. So for example, if you are sending this JSON content: @@ -143,15 +143,15 @@ So for example, if you are sending this JSON content: { "company": { "name": "acme", "address": "123 Carrot Street" } } ``` -You'll get `params[:company]` as `{ "name" => "acme", "address" => "123 Carrot Street" }`. +Your controller will receive `params[:company]` as `{ "name" => "acme", "address" => "123 Carrot Street" }`. -Also, if you've turned on `config.wrap_parameters` in your initializer or calling `wrap_parameters` in your controller, you can safely omit the root element in the JSON parameter. The parameters will be cloned and wrapped in the key according to your controller's name by default. So the above parameter can be written as: +Also, if you've turned on `config.wrap_parameters` in your initializer or called `wrap_parameters` in your controller, you can safely omit the root element in the JSON parameter. In this case, the parameters will be cloned and wrapped with a key chosen based on your controller's name. So the above JSON POST can be written as: ```json { "name": "acme", "address": "123 Carrot Street" } ``` -And assume that you're sending the data to `CompaniesController`, it would then be wrapped in `:company` key like this: +And, assuming that you're sending the data to `CompaniesController`, it would then be wrapped within the `:company` key like this: ```ruby { name: "acme", address: "123 Carrot Street", company: { name: "acme", address: "123 Carrot Street" } } @@ -159,17 +159,17 @@ And assume that you're sending the data to `CompaniesController`, it would then You can customize the name of the key or specific parameters you want to wrap by consulting the [API documentation](http://api.rubyonrails.org/classes/ActionController/ParamsWrapper.html) -NOTE: Support for parsing XML parameters has been extracted into a gem named `actionpack-xml_parser` +NOTE: Support for parsing XML parameters has been extracted into a gem named `actionpack-xml_parser`. ### Routing Parameters -The `params` hash will always contain the `:controller` and `:action` keys, but you should use the methods `controller_name` and `action_name` instead to access these values. Any other parameters defined by the routing, such as `:id` will also be available. As an example, consider a listing of clients where the list can show either active or inactive clients. We can add a route which captures the `:status` parameter in a "pretty" URL: +The `params` hash will always contain the `:controller` and `:action` keys, but you should use the methods `controller_name` and `action_name` instead to access these values. Any other parameters defined by the routing, such as `:id`, will also be available. As an example, consider a listing of clients where the list can show either active or inactive clients. We can add a route which captures the `:status` parameter in a "pretty" URL: ```ruby get '/clients/:status' => 'clients#index', foo: 'bar' ``` -In this case, when a user opens the URL `/clients/active`, `params[:status]` will be set to "active". When this route is used, `params[:foo]` will also be set to "bar" just like it was passed in the query string. In the same way `params[:action]` will contain "index". +In this case, when a user opens the URL `/clients/active`, `params[:status]` will be set to "active". When this route is used, `params[:foo]` will also be set to "bar", as if it were passed in the query string. Your controller will also receive `params[:action]` as "index" and `params[:controller]` as "clients". ### `default_url_options` @@ -183,21 +183,21 @@ class ApplicationController < ActionController::Base end ``` -These options will be used as a starting point when generating URLs, so it's possible they'll be overridden by the options passed in `url_for` calls. +These options will be used as a starting point when generating URLs, so it's possible they'll be overridden by the options passed to `url_for` calls. -If you define `default_url_options` in `ApplicationController`, as in the example above, it would be used for all URL generation. The method can also be defined in one specific controller, in which case it only affects URLs generated there. +If you define `default_url_options` in `ApplicationController`, as in the example above, it will be used for all URL generation. The method can also be defined in a specific controller, in which case it only affects URLs generated there. ### Strong Parameters With strong parameters, Action Controller parameters are forbidden to be used in Active Model mass assignments until they have been -whitelisted. This means you'll have to make a conscious choice about -which attributes to allow for mass updating and thus prevent -accidentally exposing that which shouldn't be exposed. +whitelisted. This means that you'll have to make a conscious decision about +which attributes to allow for mass update. This is a better security +practice to help prevent accidentally allowing users to update sensitive +model attributes. -In addition, parameters can be marked as required and flow through a -predefined raise/rescue flow to end up as a 400 Bad Request with no -effort. +In addition, parameters can be marked as required and will flow through a +predefined raise/rescue flow to end up as a 400 Bad Request. ```ruby class PeopleController < ActionController::Base @@ -239,17 +239,17 @@ params.permit(:id) ``` the key `:id` will pass the whitelisting if it appears in `params` and -it has a permitted scalar value associated. Otherwise the key is going +it has a permitted scalar value associated. Otherwise, the key is going to be filtered out, so arrays, hashes, or any other objects cannot be injected. The permitted scalar types are `String`, `Symbol`, `NilClass`, `Numeric`, `TrueClass`, `FalseClass`, `Date`, `Time`, `DateTime`, -`StringIO`, `IO`, `ActionDispatch::Http::UploadedFile` and +`StringIO`, `IO`, `ActionDispatch::Http::UploadedFile`, and `Rack::Test::UploadedFile`. To declare that the value in `params` must be an array of permitted -scalar values map the key to an empty array: +scalar values, map the key to an empty array: ```ruby params.permit(id: []) @@ -262,14 +262,13 @@ used: params.require(:log_entry).permit! ``` -This will mark the `:log_entry` parameters hash and any sub-hash of it -permitted. Extreme care should be taken when using `permit!` as it -will allow all current and future model attributes to be -mass-assigned. +This will mark the `:log_entry` parameters hash and any sub-hash of it as +permitted. Extreme care should be taken when using `permit!`, as it +will allow all current and future model attributes to be mass-assigned. #### Nested Parameters -You can also use permit on nested parameters, like: +You can also use `permit` on nested parameters, like: ```ruby params.permit(:name, { emails: [] }, @@ -277,19 +276,19 @@ params.permit(:name, { emails: [] }, { family: [ :name ], hobbies: [] }]) ``` -This declaration whitelists the `name`, `emails` and `friends` +This declaration whitelists the `name`, `emails`, and `friends` attributes. It is expected that `emails` will be an array of permitted -scalar values and that `friends` will be an array of resources with -specific attributes : they should have a `name` attribute (any +scalar values, and that `friends` will be an array of resources with +specific attributes: they should have a `name` attribute (any permitted scalar values allowed), a `hobbies` attribute as an array of permitted scalar values, and a `family` attribute which is restricted -to having a `name` (any permitted scalar values allowed, too). +to having a `name` (any permitted scalar values allowed here, too). #### More Examples -You want to also use the permitted attributes in the `new` +You may want to also use the permitted attributes in your `new` action. This raises the problem that you can't use `require` on the -root key because normally it does not exist when calling `new`: +root key because, normally, it does not exist when calling `new`: ```ruby # using `fetch` you can supply a default and use @@ -297,8 +296,8 @@ root key because normally it does not exist when calling `new`: params.fetch(:blog, {}).permit(:title, :author) ``` -`accepts_nested_attributes_for` allows you to update and destroy -associated records. This is based on the `id` and `_destroy` +The model class method `accepts_nested_attributes_for` allows you to +update and destroy associated records. This is based on the `id` and `_destroy` parameters: ```ruby @@ -306,7 +305,7 @@ parameters: params.require(:author).permit(:name, books_attributes: [:title, :id, :_destroy]) ``` -Hashes with integer keys are treated differently and you can declare +Hashes with integer keys are treated differently, and you can declare the attributes as if they were direct children. You get these kinds of parameters when you use `accepts_nested_attributes_for` in combination with a `has_many` association: @@ -323,13 +322,13 @@ params.require(:book).permit(:title, chapters_attributes: [:title]) #### Outside the Scope of Strong Parameters The strong parameter API was designed with the most common use cases -in mind. It is not meant as a silver bullet to handle all your -whitelisting problems. However you can easily mix the API with your +in mind. It is not meant as a silver bullet to handle all of your +whitelisting problems. However, you can easily mix the API with your own code to adapt to your situation. Imagine a scenario where you have parameters representing a product name and a hash of arbitrary data associated with that product, and -you want to whitelist the product name attribute but also the whole +you want to whitelist the product name attribute and also the whole data hash. The strong parameters API doesn't let you directly whitelist the whole of a nested hash with any keys, but you can use the keys of your nested hash to declare what to whitelist: @@ -668,11 +667,11 @@ You may notice in the above code that we're using `render xml: @users`, not `ren Filters ------- -Filters are methods that are run before, after or "around" a controller action. +Filters are methods that are run "before", "after" or "around" a controller action. Filters are inherited, so if you set a filter on `ApplicationController`, it will be run on every controller in your application. -"Before" filters may halt the request cycle. A common "before" filter is one which requires that a user is logged in for an action to be run. You can define the filter method this way: +"before" filters may halt the request cycle. A common "before" filter is one which requires that a user is logged in for an action to be run. You can define the filter method this way: ```ruby class ApplicationController < ActionController::Base @@ -705,9 +704,9 @@ Now, the `LoginsController`'s `new` and `create` actions will work as before wit In addition to "before" filters, you can also run filters after an action has been executed, or both before and after. -"After" filters are similar to "before" filters, but because the action has already been run they have access to the response data that's about to be sent to the client. Obviously, "after" filters cannot stop the action from running. +"after" filters are similar to "before" filters, but because the action has already been run they have access to the response data that's about to be sent to the client. Obviously, "after" filters cannot stop the action from running. -"Around" filters are responsible for running their associated actions by yielding, similar to how Rack middlewares work. +"around" filters are responsible for running their associated actions by yielding, similar to how Rack middlewares work. For example, in a website where changes have an approval workflow an administrator could be able to preview them easily, just apply them within a transaction: diff --git a/guides/source/action_view_overview.md b/guides/source/action_view_overview.md index 8f6676dc65..44c02165db 100644 --- a/guides/source/action_view_overview.md +++ b/guides/source/action_view_overview.md @@ -182,7 +182,7 @@ One way to use partials is to treat them as the equivalent of subroutines; a way <p>Here are a few of our fine products:</p> <% @products.each do |product| %> - <%= render partial: "product", locals: {product: product} %> + <%= render partial: "product", locals: { product: product } %> <% end %> <%= render "shared/footer" %> @@ -197,7 +197,7 @@ these are the only options you want to pass, you can skip using these options. For example, instead of: ```erb -<%= render partial: "product", locals: {product: @product} %> +<%= render partial: "product", locals: { product: @product } %> ``` You can also do: @@ -217,7 +217,7 @@ By default `ActionView::Partials::PartialRenderer` has its object in a local var within product we'll get `@product` in the local variable `product`, as if we had written: ```erb -<%= render partial: "product", locals: {product: @product} %> +<%= render partial: "product", locals: { product: @product } %> ``` With the `as` option we can specify a different name for the local variable. For example, if we wanted it to be `item` instead of `product` we would do: @@ -231,7 +231,7 @@ The `object` option can be used to directly specify which object is rendered int For example, instead of: ```erb -<%= render partial: "product", locals: {product: @item} %> +<%= render partial: "product", locals: { product: @item } %> ``` we would do: @@ -304,7 +304,7 @@ In the `show` template, we'll render the `_article` partial wrapped in the `box` **articles/show.html.erb** ```erb -<%= render partial: 'article', layout: 'box', locals: {article: @article} %> +<%= render partial: 'article', layout: 'box', locals: { article: @article } %> ``` The `box` layout simply wraps the `_article` partial in a `div`: @@ -344,7 +344,7 @@ You can also render a block of code within a partial layout instead of calling ` **articles/show.html.erb** ```html+erb -<% render(layout: 'box', locals: {article: @article}) do %> +<% render(layout: 'box', locals: { article: @article }) do %> <%= div_for(article) do %> <p><%= article.body %></p> <% end %> @@ -356,7 +356,39 @@ Supposing we use the same `_box` partial from above, this would produce the same View Paths ---------- -TODO... +When rendering the view for a request, the controller needs to resolve where to find each of the directories are located. + +We are able to modify the order these locations are resolved by using `prepend_view_path` and `append_view_path`. + +This allows us to add new paths to the beginning or end of the list used to resolve these paths. + +### Prepend view path + +This can be helpful for example, when we want to prepend a different directory for subdomains. + +We can do this by using: + +```prepend_view_path "app/views/#{request.subdomain}"``` + +Then our list becomes something like: + +``` +[ + ~/rails_app/app/views/<subdomain>, + ~/rails_app/app/views, + # ... +] +``` + +This will put the subdomain path at the beginning of the list. + +### Append view path + +Similarly, we can append paths: + +```append_view_path "app/views/direct"```. + +This will add ```app/views/direct``` and the end of lookup paths for views. Overview of helpers provided by Action View ------------------------------------------- @@ -376,39 +408,13 @@ config.action_controller.asset_host = "assets.example.com" image_tag("rails.png") # => <img src="http://assets.example.com/images/rails.png" alt="Rails" /> ``` -#### register_javascript_expansion - -Register one or more JavaScript files to be included when symbol is passed to javascript_include_tag. This method is typically intended to be called from plugin initialization to register JavaScript files that the plugin installed in `vendor/assets/javascripts`. - -```ruby -ActionView::Helpers::AssetTagHelper.register_javascript_expansion monkey: ["head", "body", "tail"] - -javascript_include_tag :monkey # => - <script src="/assets/head.js"></script> - <script src="/assets/body.js"></script> - <script src="/assets/tail.js"></script> -``` - -#### register_stylesheet_expansion - -Register one or more stylesheet files to be included when symbol is passed to `stylesheet_link_tag`. This method is typically intended to be called from plugin initialization to register stylesheet files that the plugin installed in `vendor/assets/stylesheets`. - -```ruby -ActionView::Helpers::AssetTagHelper.register_stylesheet_expansion monkey: ["head", "body", "tail"] - -stylesheet_link_tag :monkey # => - <link href="/assets/head.css" media="screen" rel="stylesheet" /> - <link href="/assets/body.css" media="screen" rel="stylesheet" /> - <link href="/assets/tail.css" media="screen" rel="stylesheet" /> -``` - #### auto_discovery_link_tag Returns a link tag that browsers and feed readers can use to auto-detect an RSS or Atom feed. ```ruby -auto_discovery_link_tag(:rss, "http://www.example.com/feed.rss", {title: "RSS Feed"}) # => - <link rel="alternate" type="application/rss+xml" title="RSS Feed" href="http://www.example.com/feed" /> +auto_discovery_link_tag(:rss, "http://www.example.com/feed.rss", { title: "RSS Feed" }) # => + <link rel="alternate" type="application/rss+xml" title="RSS Feed" href="http://www.example.com/feed.rss" /> ``` #### image_path @@ -789,7 +795,7 @@ time_select("order", "submitted") Returns a `pre` tag that has object dumped by YAML. This creates a very readable way to inspect an object. ```ruby -my_hash = {'first' => 1, 'second' => 'two', 'third' => [1,2,3]} +my_hash = { 'first' => 1, 'second' => 'two', 'third' => [1,2,3] } debug(my_hash) ``` @@ -814,7 +820,7 @@ The core method of this helper, form_for, gives you the ability to create a form ```html+erb # Note: a @person variable will have been created in the controller (e.g. @person = Person.new) -<%= form_for @person, url: {action: "create"} do |f| %> +<%= form_for @person, url: { action: "create" } do |f| %> <%= f.text_field :first_name %> <%= f.text_field :last_name %> <%= submit_tag 'Create' %> @@ -834,7 +840,7 @@ The HTML generated for this would be: The params object created when this form is submitted would look like: ```ruby -{"action" => "create", "controller" => "people", "person" => {"first_name" => "William", "last_name" => "Smith"}} +{ "action" => "create", "controller" => "people", "person" => { "first_name" => "William", "last_name" => "Smith" } } ``` The params hash has a nested person value, which can therefore be accessed with params[:person] in the controller. @@ -855,7 +861,7 @@ check_box("article", "validated") Creates a scope around a specific model object like form_for, but doesn't create the form tags themselves. This makes fields_for suitable for specifying additional model objects in the same form: ```html+erb -<%= form_for @person, url: {action: "update"} do |person_form| %> +<%= form_for @person, url: { action: "update" } do |person_form| %> First name: <%= person_form.text_field :first_name %> Last name : <%= person_form.text_field :last_name %> @@ -990,7 +996,7 @@ end Sample usage (selecting the associated Author for an instance of Article, `@article`): ```ruby -collection_select(:article, :author_id, Author.all, :id, :name_with_initial, {prompt: true}) +collection_select(:article, :author_id, Author.all, :id, :name_with_initial, { prompt: true }) ``` If `@article.author_id` is 1, this would return: @@ -1162,7 +1168,7 @@ Create a select tag and a series of contained option tags for the provided objec Example: ```ruby -select("article", "person_id", Person.all.collect {|p| [ p.name, p.id ] }, {include_blank: true}) +select("article", "person_id", Person.all.collect { |p| [ p.name, p.id ] }, { include_blank: true }) ``` If `@article.person_id` is 1, this would become: @@ -1225,7 +1231,7 @@ Creates a field set for grouping HTML form elements. Creates a file upload field. ```html+erb -<%= form_tag({action:"post"}, multipart: true) do %> +<%= form_tag({ action: "post" }, multipart: true) do %> <label for="file">File to Upload</label> <%= file_field_tag "file" %> <%= submit_tag %> <% end %> @@ -1361,22 +1367,6 @@ date_field_tag "dob" Provides functionality for working with JavaScript in your views. -#### button_to_function - -Returns a button that'll trigger a JavaScript function using the onclick handler. Examples: - -```ruby -button_to_function "Greeting", "alert('Hello world!')" -button_to_function "Delete", "if (confirm('Really?')) do_delete()" -button_to_function "Details" do |page| - page[:details].visual_effect :toggle_slide -end -``` - -#### define_javascript_functions - -Includes the Action Pack JavaScript libraries inside a single `script` tag. - #### escape_javascript Escape carrier returns and single and double quotes for JavaScript segments. @@ -1397,15 +1387,6 @@ alert('All is good') </script> ``` -#### link_to_function - -Returns a link that will trigger a JavaScript function using the onclick handler and return false after the fact. - -```ruby -link_to_function "Greeting", "alert('Hello world!')" -# => <a onclick="alert('Hello world!'); return false;" href="#">Greeting</a> -``` - ### NumberHelper Provides methods for converting numbers into formatted strings. Methods are provided for phone numbers, currency, percentage, precision, positional notation, and file size. diff --git a/guides/source/active_job_basics.md b/guides/source/active_job_basics.md index 953c29719d..29d0c32b09 100644 --- a/guides/source/active_job_basics.md +++ b/guides/source/active_job_basics.md @@ -11,7 +11,7 @@ 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. +* How to send emails from your application asynchronously. -------------------------------------------------------------------------------- @@ -319,3 +319,10 @@ class GuestsCleanupJob < ActiveJob::Base end end ``` + + +Job Testing +-------------- + +You can find detailed instructions on how to test your jobs in the +[testing guide](testing.html#testing-jobs). diff --git a/guides/source/active_record_basics.md b/guides/source/active_record_basics.md index bd8cdf62f2..6551ba0389 100644 --- a/guides/source/active_record_basics.md +++ b/guides/source/active_record_basics.md @@ -20,7 +20,7 @@ After reading this guide, you will know: What is Active Record? ---------------------- -Active Record is the M in [MVC](getting_started.html#the-mvc-architecture) - the +Active Record is the M in [MVC](http://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller) - the model - which is the layer of the system responsible for representing business data and logic. Active Record facilitates the creation and use of business objects whose data requires persistent storage to a database. It is an @@ -122,7 +122,7 @@ to Active Record instances: * `(association_name)_type` - Stores the type for [polymorphic associations](association_basics.html#polymorphic-associations). * `(table_name)_count` - Used to cache the number of belonging objects on - associations. For example, a `comments_count` column in a `Articles` class that + associations. For example, a `comments_count` column in an `Article` class that has many instances of `Comment` will cache the number of existent comments for each article. @@ -173,18 +173,18 @@ name that should be used: ```ruby class Product < ActiveRecord::Base - self.table_name = "PRODUCT" + self.table_name = "my_products" end ``` If you do so, you will have to define manually the class name that is hosting -the fixtures (class_name.yml) using the `set_fixture_class` method in your test +the fixtures (my_products.yml) using the `set_fixture_class` method in your test definition: ```ruby -class FunnyJoke < ActiveSupport::TestCase - set_fixture_class funny_jokes: Joke - fixtures :funny_jokes +class ProductTest < ActiveSupport::TestCase + set_fixture_class my_products: Product + fixtures :my_products ... end ``` diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index e65ab802c0..13989a3b33 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -68,7 +68,7 @@ class User < ActiveRecord::Base protected def normalize_name - self.name = self.name.downcase.titleize + self.name = name.downcase.titleize end def set_location diff --git a/guides/source/active_record_migrations.md b/guides/source/active_record_migrations.md index b8db21a989..7a994cc5de 100644 --- a/guides/source/active_record_migrations.md +++ b/guides/source/active_record_migrations.md @@ -241,7 +241,7 @@ generates ```ruby class AddUserRefToProducts < ActiveRecord::Migration def change - add_reference :products, :user, index: true + add_reference :products, :user, index: true, foreign_key: true end end ``` @@ -357,7 +357,7 @@ will append `ENGINE=BLACKHOLE` to the SQL statement used to create the table ### Creating a Join Table -Migration method `create_join_table` creates a HABTM join table. A typical use +Migration method `create_join_table` creates an HABTM join table. A typical use would be: ```ruby @@ -425,7 +425,7 @@ change_column :products, :part_number, :text This changes the column `part_number` on products table to be a `:text` field. Besides `change_column`, the `change_column_null` and `change_column_default` -methods are used specifically to change the null and default values of a +methods are used specifically to change a not null constraint and default values of a column. ```ruby @@ -501,7 +501,7 @@ If the helpers provided by Active Record aren't enough you can use the `execute` method to execute arbitrary SQL: ```ruby -Product.connection.execute('UPDATE `products` SET `price`=`free` WHERE 1') +Product.connection.execute("UPDATE products SET price = 'free' WHERE 1=1") ``` For more details and examples of individual methods, check the API documentation. @@ -539,6 +539,14 @@ definitions: `change_table` is also reversible, as long as the block does not call `change`, `change_default` or `remove`. +`remove_column` is reversible if you supply the column type as the third +argument. Provide the original column options too, otherwise Rails can't +recreate the column exactly when rolling back: + +```ruby +remove_column :posts, :slug, :string, null: false, default: '', index: true +``` + If you're going to need to use any other methods, you should use `reversible` or write the `up` and `down` methods instead of using the `change` method. diff --git a/guides/source/active_record_postgresql.md b/guides/source/active_record_postgresql.md index 4d9c1776f4..66a11e5785 100644 --- a/guides/source/active_record_postgresql.md +++ b/guides/source/active_record_postgresql.md @@ -245,6 +245,7 @@ article.save! * [type definition](http://www.postgresql.org/docs/9.3/static/datatype-uuid.html) * [generator functions](http://www.postgresql.org/docs/9.3/static/uuid-ossp.html) +NOTE: you need to enable the `uuid-ossp` extension to use uuid. ```ruby # db/migrate/20131220144913_create_revisions.rb @@ -263,6 +264,28 @@ revision = Revision.first revision.identifier # => "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11" ``` +You can use `uuid` type to define references in migrations + +```ruby +# db/migrate/20150418012400_create_blog.rb +create_table :posts, id: :uuid + +create_table :comments, id: :uuid do |t| + # t.belongs_to :post, type: :uuid + t.references :post, type: :uuid +end + +# app/models/post.rb +class Post < ActiveRecord::Base + has_many :comments +end + +# app/models/comment.rb +class Comment < ActiveRecord::Base + belongs_to :post +end +``` + ### Bit String Types * [type definition](http://www.postgresql.org/docs/9.3/static/datatype-bit.html) diff --git a/guides/source/active_record_querying.md b/guides/source/active_record_querying.md index e5a962b739..2f10bc4e7c 100644 --- a/guides/source/active_record_querying.md +++ b/guides/source/active_record_querying.md @@ -317,7 +317,7 @@ end The `find_each` method accepts most of the options allowed by the regular `find` method, except for `:order` and `:limit`, which are reserved for internal use by `find_each`. -Two additional options, `:batch_size` and `:begin_at`, are available as well. +Three additional options, `:batch_size`, `:begin_at` and `:end_at`, are available as well. **`:batch_size`** @@ -348,7 +348,7 @@ Another example would be if you wanted multiple workers handling the same proces Similar to the `:begin_at` option, `:end_at` allows you to configure the last ID of the sequence whenever the highest ID is not the one you need. This would be useful, for example, if you wanted to run a batch process, using a subset of records based on `:begin_at` and `:end_at` -For example, to send newsletters only to users with the primary key starting from 2000 upto 10000 and to retrieve them in batches of 1000: +For example, to send newsletters only to users with the primary key starting from 2000 up to 10000 and to retrieve them in batches of 5000: ```ruby User.find_each(begin_at: 2000, end_at: 10000, batch_size: 5000) do |user| @@ -529,7 +529,7 @@ Client.order("orders_count ASC, created_at DESC") Client.order("orders_count ASC", "created_at DESC") ``` -If you want to call `order` multiple times e.g. in different context, new order will append previous one +If you want to call `order` multiple times e.g. in different context, new order will append previous one: ```ruby Client.order("orders_count ASC").order("created_at DESC") @@ -655,7 +655,7 @@ GROUP BY status Having ------ -SQL uses the `HAVING` clause to specify conditions on the `GROUP BY` fields. You can add the `HAVING` clause to the SQL fired by the `Model.find` by adding the `:having` option to the find. +SQL uses the `HAVING` clause to specify conditions on the `GROUP BY` fields. You can add the `HAVING` clause to the SQL fired by the `Model.find` by adding the `having` method to the find. For example: @@ -1343,7 +1343,7 @@ Client.unscoped { Dynamic Finders --------------- -For every field (also known as an attribute) you define in your table, Active Record provides a finder method. If you have a field called `first_name` on your `Client` model for example, you get `find_by_first_name` for free from Active Record. If you have a `locked` field on the `Client` model, you also get `find_by_locked` and methods. +For every field (also known as an attribute) you define in your table, Active Record provides a finder method. If you have a field called `first_name` on your `Client` model for example, you get `find_by_first_name` for free from Active Record. If you have a `locked` field on the `Client` model, you also get `find_by_locked` method. You can specify an exclamation point (`!`) on the end of the dynamic finders to get them to raise an `ActiveRecord::RecordNotFound` error if they do not return any records, like `Client.find_by_name!("Ryan")` diff --git a/guides/source/active_record_validations.md b/guides/source/active_record_validations.md index f19934fe89..d251c5c0b1 100644 --- a/guides/source/active_record_validations.md +++ b/guides/source/active_record_validations.md @@ -1055,7 +1055,9 @@ person.errors[:name] ### `errors.add` -The `add` method lets you manually add messages that are related to particular attributes. You can use the `errors.full_messages` or `errors.to_a` methods to view the messages in the form they might be displayed to a user. Those particular messages get the attribute name prepended (and capitalized). `add` receives the name of the attribute you want to add the message to, and the message itself. +The `add` method lets you add an error message related to a particular attribute. It takes as arguments the attribute and the error message. + +The `errors.full_messages` method (or its equivalent, `errors.to_a`) returns the error messages in a user-friendly format, with the capitalized attribute name prepended to each message, as shown in the examples below. ```ruby class Person < ActiveRecord::Base @@ -1073,12 +1075,12 @@ person.errors.full_messages # => ["Name cannot contain the characters !@#%*()_-+="] ``` -Another way to do this is using `[]=` setter +An equivalent to `errors#add` is to use `<<` to append a message to the `errors.messages` array for an attribute: ```ruby class Person < ActiveRecord::Base def a_method_used_for_validation_purposes - errors.add(:name, "cannot contain the characters !@#%*()_-+=") + errors.messages[:name] << "cannot contain the characters !@#%*()_-+=" end end diff --git a/guides/source/active_support_core_extensions.md b/guides/source/active_support_core_extensions.md index 66626f41d1..2a643680f7 100644 --- a/guides/source/active_support_core_extensions.md +++ b/guides/source/active_support_core_extensions.md @@ -475,7 +475,7 @@ The methods `silence_warnings` and `enable_warnings` change the value of `$VERBO silence_warnings { Object.const_set "RAILS_DEFAULT_LOGGER", logger } ``` -Silencing exceptions is also possible with `suppress`. This method receives an arbitrary number of exception classes. If an exception is raised during the execution of the block and is `kind_of?` any of the arguments, `suppress` captures it and returns silently. Otherwise the exception is reraised: +Silencing exceptions is also possible with `suppress`. This method receives an arbitrary number of exception classes. If an exception is raised during the execution of the block and is `kind_of?` any of the arguments, `suppress` captures it and returns silently. Otherwise the exception is not captured: ```ruby # If the user is locked, the increment is lost, no big deal. @@ -506,6 +506,8 @@ Extensions to `Module` ### `alias_method_chain` +**This method is deprecated in favour of using Module#prepend.** + Using plain Ruby you can wrap methods with other methods, that's called _alias chaining_. For example, let's say you'd like params to be strings in functional tests, as they are in real requests, but still want the convenience of assigning integers and other kind of values. To accomplish that you could wrap `ActionController::TestCase#process` this way in `test/test_helper.rb`: @@ -550,8 +552,6 @@ ActionController::TestCase.class_eval do end ``` -Rails uses `alias_method_chain` all over the code base. For example validations are added to `ActiveRecord::Base#save` by wrapping the method that way in a separate module specialized in validations. - NOTE: Defined in `active_support/core_ext/module/aliasing.rb`. ### Attributes @@ -2188,7 +2188,7 @@ The method `without` returns a copy of an enumerable with the specified elements removed: ```ruby -people.without("Aaron", "Todd") +["David", "Rafael", "Aaron", "Todd"].without("Aaron", "Todd") # => ["David", "Rafael"] ``` NOTE: Defined in `active_support/core_ext/enumerable.rb`. @@ -2428,7 +2428,7 @@ The method `Array.wrap` wraps its argument in an array unless it is already an a Specifically: -* If the argument is `nil` an empty list is returned. +* If the argument is `nil` an empty array is returned. * Otherwise, if the argument responds to `to_ary` it is invoked, and if the value of `to_ary` is not `nil`, it is returned. * Otherwise, an array with the argument as its single element is returned. @@ -2440,9 +2440,9 @@ Array.wrap(0) # => [0] This method is similar in purpose to `Kernel#Array`, but there are some differences: -* If the argument responds to `to_ary` the method is invoked. `Kernel#Array` moves on to try `to_a` if the returned value is `nil`, but `Array.wrap` returns `nil` right away. +* If the argument responds to `to_ary` the method is invoked. `Kernel#Array` moves on to try `to_a` if the returned value is `nil`, but `Array.wrap` returns an array with the argument as its single element right away. * If the returned value from `to_ary` is neither `nil` nor an `Array` object, `Kernel#Array` raises an exception, while `Array.wrap` does not, it just returns the value. -* It does not call `to_a` on the argument, though special-cases `nil` to return an empty array. +* It does not call `to_a` on the argument, if the argument does not respond to +to_ary+ it returns an array with the argument as its single element. The last point is particularly worth comparing for some enumerables: @@ -3040,53 +3040,6 @@ The method `Range#overlaps?` says whether any two given ranges have non-void int NOTE: Defined in `active_support/core_ext/range/overlaps.rb`. -Extensions to `Proc` --------------------- - -### `bind` - -As you surely know Ruby has an `UnboundMethod` class whose instances are methods that belong to the limbo of methods without a self. The method `Module#instance_method` returns an unbound method for example: - -```ruby -Hash.instance_method(:delete) # => #<UnboundMethod: Hash#delete> -``` - -An unbound method is not callable as is, you need to bind it first to an object with `bind`: - -```ruby -clear = Hash.instance_method(:clear) -clear.bind({a: 1}).call # => {} -``` - -Active Support defines `Proc#bind` with an analogous purpose: - -```ruby -Proc.new { size }.bind([]).call # => 0 -``` - -As you see that's callable and bound to the argument, the return value is indeed a `Method`. - -NOTE: To do so `Proc#bind` actually creates a method under the hood. If you ever see a method with a weird name like `__bind_1256598120_237302` in a stack trace you know now where it comes from. - -Action Pack uses this trick in `rescue_from` for example, which accepts the name of a method and also a proc as callbacks for a given rescued exception. It has to call them in either case, so a bound method is returned by `handler_for_rescue`, thus simplifying the code in the caller: - -```ruby -def handler_for_rescue(exception) - _, rescuer = Array(rescue_handlers).reverse.detect do |klass_name, handler| - ... - end - - case rescuer - when Symbol - method(rescuer) - when Proc - rescuer.bind(self) - end -end -``` - -NOTE: Defined in `active_support/core_ext/proc.rb`. - Extensions to `Date` -------------------- diff --git a/guides/source/active_support_instrumentation.md b/guides/source/active_support_instrumentation.md index 352da43b5f..1b14bedfbf 100644 --- a/guides/source/active_support_instrumentation.md +++ b/guides/source/active_support_instrumentation.md @@ -19,7 +19,7 @@ After reading this guide, you will know: Introduction to instrumentation ------------------------------- -The instrumentation API provided by Active Support allows developers to provide hooks which other developers may hook into. There are several of these within the Rails framework, as described below in (TODO: link to section detailing each hook point). With this API, developers can choose to be notified when certain events occur inside their application or another piece of Ruby code. +The instrumentation API provided by Active Support allows developers to provide hooks which other developers may hook into. There are several of these within the [Rails framework](#rails-framework-hooks). With this API, developers can choose to be notified when certain events occur inside their application or another piece of Ruby code. For example, there is a hook provided within Active Record that is called every time Active Record uses an SQL query on a database. This hook could be **subscribed** to, and used to track the number of queries during a certain action. There's another hook around the processing of an action of a controller. This could be used, for instance, to track how long a specific action has taken. diff --git a/guides/source/asset_pipeline.md b/guides/source/asset_pipeline.md index 6f8b4f4d15..4a610e8458 100644 --- a/guides/source/asset_pipeline.md +++ b/guides/source/asset_pipeline.md @@ -149,7 +149,7 @@ clients to fetch them again, even when the content of those assets has not chang Fingerprinting fixes these problems by avoiding query strings, and by ensuring that filenames are consistent based on their content. -Fingerprinting is enabled by default for both the development and production +Fingerprinting is enabled by default for both the development and production environments. You can enable or disable it in your configuration through the `config.assets.digest` option. @@ -209,7 +209,7 @@ precompiling works. NOTE: You must have an ExecJS supported runtime in order to use CoffeeScript. If you are using Mac OS X or Windows, you have a JavaScript runtime installed in -your operating system. Check [ExecJS](https://github.com/sstephenson/execjs#readme) documentation to know all supported JavaScript runtimes. +your operating system. Check [ExecJS](https://github.com/rails/execjs#readme) documentation to know all supported JavaScript runtimes. You can also disable generation of controller specific asset files by adding the following to your `config/application.rb` configuration: @@ -643,7 +643,7 @@ above. By default Rails assumes assets have been precompiled and will be served as static assets by your web server. During the precompilation phase an MD5 is generated from the contents of the -compiled files, and inserted into the filenames as they are written to disc. +compiled files, and inserted into the filenames as they are written to disk. These fingerprinted names are used by the Rails helpers in place of the manifest name. @@ -667,8 +667,7 @@ anymore, delete these options from the `javascript_include_tag` and `stylesheet_link_tag`. The fingerprinting behavior is controlled by the `config.assets.digest` -initialization option (which defaults to `true` for production and `false` for -everything else). +initialization option (which defaults to `true` for production and development). NOTE: Under normal circumstances the default `config.assets.digest` option should not be changed. If there are no digests in the filenames, and far-future @@ -791,41 +790,6 @@ location ~ ^/assets/ { } ``` -#### GZip Compression - -When files are precompiled, Sprockets also creates a -[gzipped](http://en.wikipedia.org/wiki/Gzip) (.gz) version of your assets. Web -servers are typically configured to use a moderate compression ratio as a -compromise, but since precompilation happens once, Sprockets uses the maximum -compression ratio, thus reducing the size of the data transfer to the minimum. -On the other hand, web servers can be configured to serve compressed content -directly from disk, rather than deflating non-compressed files themselves. - -NGINX is able to do this automatically enabling `gzip_static`: - -```nginx -location ~ ^/(assets)/ { - root /path/to/public; - gzip_static on; # to serve pre-gzipped version - expires max; - add_header Cache-Control public; -} -``` - -This directive is available if the core module that provides this feature was -compiled with the web server. Ubuntu/Debian packages, even `nginx-light`, have -the module compiled. Otherwise, you may need to perform a manual compilation: - -```bash -./configure --with-http_gzip_static_module -``` - -If you're compiling NGINX with Phusion Passenger you'll need to pass that option -when prompted. - -A robust configuration for Apache is possible but tricky; please Google around. -(Or help update this Guide if you have a good configuration example for Apache.) - ### Local Precompilation There are several reasons why you might want to precompile your assets locally. @@ -921,7 +885,7 @@ focus on serving application code as fast as possible. #### Set up a CDN to Serve Static Assets To set up your CDN you have to have your application running in production on -the internet at a publically available URL, for example `example.com`. Next +the internet at a publicly available URL, for example `example.com`. Next you'll need to sign up for a CDN service from a cloud hosting provider. When you do this you need to configure the "origin" of the CDN to point back at your website `example.com`, check your provider for documentation on configuring the @@ -974,7 +938,7 @@ http://mycdnsubdomain.fictional-cdn.com/assets/smile.png If the CDN has a copy of `smile.png` it will serve it to the browser and your server doesn't even know it was requested. If the CDN does not have a copy it -will try to find it a the "origin" `example.com/assets/smile.png` and then store +will try to find it at the "origin" `example.com/assets/smile.png` and then store it for future use. If you want to serve only some assets from your CDN, you can use custom `:host` @@ -1137,7 +1101,7 @@ The following line invokes `uglifier` for JavaScript compression. config.assets.js_compressor = :uglifier ``` -NOTE: You will need an [ExecJS](https://github.com/sstephenson/execjs#readme) +NOTE: You will need an [ExecJS](https://github.com/rails/execjs#readme) supported runtime in order to use `uglifier`. If you are using Mac OS X or Windows you have a JavaScript runtime installed in your operating system. diff --git a/guides/source/association_basics.md b/guides/source/association_basics.md index 280c3008e9..412cfd198a 100644 --- a/guides/source/association_basics.md +++ b/guides/source/association_basics.md @@ -146,6 +146,17 @@ class CreateSuppliers < ActiveRecord::Migration end ``` +Depending on the use case, you might also need to create a unique index and/or +a foreign key constraint on the supplier column for the accounts table. In this +case, the column definition might look like this: + +```ruby +create_table :accounts do |t| + t.belongs_to :supplier, index: true, unique: true, foreign_key: true + # ... +end +``` + ### The `has_many` Association A `has_many` association indicates a one-to-many connection with another model. You'll often find this association on the "other side" of a `belongs_to` association. This association indicates that each instance of the model has zero or more instances of another model. For example, in an application containing customers and orders, the customer model could be declared like this: @@ -829,6 +840,7 @@ The `belongs_to` association supports these options: * `:counter_cache` * `:dependent` * `:foreign_key` +* `:primary_key` * `:inverse_of` * `:polymorphic` * `:touch` @@ -875,18 +887,26 @@ end With this declaration, Rails will keep the cache value up to date, and then return that value in response to the `size` method. -Although the `:counter_cache` option is specified on the model that includes the `belongs_to` declaration, the actual column must be added to the _associated_ model. In the case above, you would need to add a column named `orders_count` to the `Customer` model. You can override the default column name if you need to: +Although the `:counter_cache` option is specified on the model that includes +the `belongs_to` declaration, the actual column must be added to the +_associated_ (`has_many`) model. In the case above, you would need to add a +column named `orders_count` to the `Customer` model. + +You can override the default column name by specifying a custom column name in +the `counter_cache` declaration instead of `true`. For example, to use +`count_of_orders` instead of `orders_count`: ```ruby class Order < ActiveRecord::Base belongs_to :customer, counter_cache: :count_of_orders end class Customer < ActiveRecord::Base - has_many :orders, counter_cache: :count_of_orders + has_many :orders end ``` -NOTE: You only need to specify the :counter_cache option on the "has_many side" of the association when using a custom name for the counter cache. +NOTE: You only need to specify the :counter_cache option on the `belongs_to` +side of the association. Counter cache columns are added to the containing model's list of read-only attributes through `attr_readonly`. @@ -913,6 +933,26 @@ end TIP: In any case, Rails will not create foreign key columns for you. You need to explicitly define them as part of your migrations. +##### `:primary_key` + +By convention, Rails assumes that the `id` column is used to hold the primary key +of its tables. The `:primary_key` option allows you to specify a different column. + +For example, given we have a `users` table with `guid` as the primary key. If we want a separate `todos` table to hold the foreign key `user_id` in the `guid` column, then we can use `primary_key` to achieve this like so: + +```ruby +class User < ActiveRecord::Base + self.primary_key = 'guid' # primary key is guid and not id +end + +class Todo < ActiveRecord::Base + belongs_to :user, primary_key: 'guid' +end +``` + +When we execute `@user.todos.create` then the `@todo` record will have its +`user_id` value as the `guid` value of `@user`. + ##### `:inverse_of` The `:inverse_of` option specifies the name of the `has_many` or `has_one` association that is the inverse of this association. Does not work in combination with the `:polymorphic` options. @@ -933,7 +973,7 @@ Passing `true` to the `:polymorphic` option indicates that this is a polymorphic ##### `:touch` -If you set the `:touch` option to `:true`, then the `updated_at` or `updated_on` timestamp on the associated object will be set to the current time whenever this object is saved or destroyed: +If you set the `:touch` option to `true`, then the `updated_at` or `updated_on` timestamp on the associated object will be set to the current time whenever this object is saved or destroyed: ```ruby class Order < ActiveRecord::Base @@ -1496,7 +1536,7 @@ While Rails uses intelligent defaults that will work well in most situations, th ```ruby class Customer < ActiveRecord::Base - has_many :orders, dependent: :delete_all, validate: :false + has_many :orders, dependent: :delete_all, validate: false end ``` @@ -1628,7 +1668,7 @@ You can use any of the standard [querying methods](active_record_querying.html) * `order` * `readonly` * `select` -* `uniq` +* `distinct` ##### `where` diff --git a/guides/source/autoloading_and_reloading_constants.md b/guides/source/autoloading_and_reloading_constants.md index c6149abcba..2b6d7e4044 100644 --- a/guides/source/autoloading_and_reloading_constants.md +++ b/guides/source/autoloading_and_reloading_constants.md @@ -466,9 +466,7 @@ by adding this to `config/application.rb`: config.autoload_paths << "#{Rails.root}/lib" ``` -`config.autoload_paths` is accessible from environment-specific configuration -files, but any changes made to it outside `config/application.rb` don't have any -effect. +`config.autoload_paths` is not changeable from environment-specific configuration files. The value of `autoload_paths` can be inspected. In a just generated application it is (edited): diff --git a/guides/source/command_line.md b/guides/source/command_line.md index 6f5a6b7957..315d8c14b6 100644 --- a/guides/source/command_line.md +++ b/guides/source/command_line.md @@ -402,8 +402,8 @@ INFO: You can also use `rake -T` to get the list of tasks. $ bin/rake about About your application's environment Rails version 5.0.0 -Ruby version 2.2.0 (x86_64-linux) -RubyGems version 2.4.5 +Ruby version 2.2.2 (x86_64-linux) +RubyGems version 2.4.6 Rack version 1.6 JavaScript Runtime Node.js (V8) Middleware Rack::Sendfile, ActionDispatch::Static, Rack::Lock, #<ActiveSupport::Cache::Strategy::LocalCache::Middleware:0x007ffd131a7c88>, Rack::Runtime, Rack::MethodOverride, ActionDispatch::RequestId, Rails::Rack::Logger, ActionDispatch::ShowExceptions, ActionDispatch::DebugExceptions, ActionDispatch::RemoteIp, ActionDispatch::Reloader, ActionDispatch::Callbacks, ActiveRecord::Migration::CheckPending, ActiveRecord::ConnectionAdapters::ConnectionManagement, ActiveRecord::QueryCache, ActionDispatch::Cookies, ActionDispatch::Session::CookieStore, ActionDispatch::Flash, ActionDispatch::ParamsParser, Rack::Head, Rack::ConditionalGet, Rack::ETag @@ -423,7 +423,7 @@ If you want to clear `public/assets` completely, you can use `rake assets:clobbe The most common tasks of the `db:` Rake namespace are `migrate` and `create`, and it will pay off to try out all of the migration rake tasks (`up`, `down`, `redo`, `reset`). `rake db:version` is useful when troubleshooting, telling you the current version of the database. -More information about migrations can be found in the [Migrations](migrations.html) guide. +More information about migrations can be found in the [Migrations](active_record_migrations.html) guide. ### `notes` @@ -470,7 +470,7 @@ app/models/article.rb: NOTE. When using specific annotations and custom annotations, the annotation name (FIXME, BUG etc) is not displayed in the output lines. -By default, `rake notes` will look in the `app`, `config`, `lib`, `bin` and `test` directories. If you would like to search other directories, you can provide them as a comma separated list in an environment variable `SOURCE_ANNOTATION_DIRECTORIES`. +By default, `rake notes` will look in the `app`, `config`, `db`, `lib` and `test` directories. If you would like to search other directories, you can provide them as a comma separated list in an environment variable `SOURCE_ANNOTATION_DIRECTORIES`. ```bash $ export SOURCE_ANNOTATION_DIRECTORIES='spec,vendor' @@ -526,8 +526,8 @@ end To pass arguments to your custom rake task: ```ruby -task :task_name, [:arg_1] => [:pre_1, :pre_2] do |t, args| - # You can use args from here +task :task_name, [:arg_1] => [:prerequisite_1, :prerequisite_2] do |task, args| + argument_1 = args.arg_1 end ``` diff --git a/guides/source/configuring.md b/guides/source/configuring.md index b791114ed9..938ba2c89f 100644 --- a/guides/source/configuring.md +++ b/guides/source/configuring.md @@ -302,8 +302,18 @@ All these configuration options are delegated to the `I18n` library. `config/environments/production.rb` which is generated by Rails. The default value is true if this configuration is not set. +* `config.active_record.dump_schemas` controls which database schemas will be dumped when calling db:structure:dump. + The options are `:schema_search_path` (the default) which dumps any schemas listed in schema_search_path, + `:all` which always dumps all schemas regardless of the schema_search_path, + or a string of comma separated schemas. + * `config.active_record.belongs_to_required_by_default` is a boolean value and controls whether `belongs_to` association is required by default. +* `config.active_record.warn_on_records_fetched_greater_than` allows setting a + warning threshold for query result size. If the number of records returned + by a query exceeds the threshold, a warning is logged. This can be used to + identify queries which might be causing memory bloat. + The MySQL adapter adds one additional configuration option: * `ActiveRecord::ConnectionAdapters::MysqlAdapter.emulate_booleans` controls whether Active Record will consider all `tinyint(1)` columns in a MySQL database to be booleans and is true by default. @@ -414,13 +424,23 @@ encrypted cookies salt value. Defaults to `'signed encrypted cookie'`. end ``` -* `config.action_view.default_form_builder` tells Rails which form builder to use by default. The default is `ActionView::Helpers::FormBuilder`. If you want your form builder class to be loaded after initialization (so it's reloaded on each request in development), you can pass it as a `String` +* `config.action_view.default_form_builder` tells Rails which form builder to + use by default. The default is `ActionView::Helpers::FormBuilder`. If you + want your form builder class to be loaded after initialization (so it's + reloaded on each request in development), you can pass it as a `String`. * `config.action_view.logger` accepts a logger conforming to the interface of Log4r or the default Ruby Logger class, which is then used to log information from Action View. Set to `nil` to disable logging. * `config.action_view.erb_trim_mode` gives the trim mode to be used by ERB. It defaults to `'-'`, which turns on trimming of tail spaces and newline when using `<%= -%>` or `<%= =%>`. See the [Erubis documentation](http://www.kuwata-lab.com/erubis/users-guide.06.html#topics-trimspaces) for more information. -* `config.action_view.embed_authenticity_token_in_remote_forms` allows you to set the default behavior for `authenticity_token` in forms with `:remote => true`. By default it's set to false, which means that remote forms will not include `authenticity_token`, which is helpful when you're fragment-caching the form. Remote forms get the authenticity from the `meta` tag, so embedding is unnecessary unless you support browsers without JavaScript. In such case you can either pass `:authenticity_token => true` as a form option or set this config setting to `true` +* `config.action_view.embed_authenticity_token_in_remote_forms` allows you to + set the default behavior for `authenticity_token` in forms with `remote: + true`. By default it's set to false, which means that remote forms will not + include `authenticity_token`, which is helpful when you're fragment-caching + the form. Remote forms get the authenticity from the `meta` tag, so embedding + is unnecessary unless you support browsers without JavaScript. In such case + you can either pass `authenticity_token: true` as a form option or set this + config setting to `true`. * `config.action_view.prefix_partial_path_with_controller_namespace` determines whether or not partials are looked up from a subdirectory in templates rendered from namespaced controllers. For example, consider a controller named `Admin::ArticlesController` which renders this template: @@ -430,7 +450,8 @@ encrypted cookies salt value. Defaults to `'signed encrypted cookie'`. The default setting is `true`, which uses the partial at `/admin/articles/_article.erb`. Setting the value to `false` would render `/articles/_article.erb`, which is the same behavior as rendering from a non-namespaced controller such as `ArticlesController`. -* `config.action_view.raise_on_missing_translations` determines whether an error should be raised for missing translations +* `config.action_view.raise_on_missing_translations` determines whether an + error should be raised for missing translations. ### Configuring Action Mailer @@ -523,6 +544,58 @@ There are a few configuration options available in Active Support: * `ActiveSupport::Deprecation.silenced` sets whether or not to display deprecation warnings. +### Configuring Active Job + +`config.active_job` provides the following configuration options: + +* `config.active_job.queue_adapter` sets the adapter for the queueing backend. The default adapter is `:inline` which will perform jobs immediately. For an up-to-date list of built-in adapters see the [ActiveJob::QueueAdapters API documentation](http://api.rubyonrails.org/classes/ActiveJob/QueueAdapters.html). + + ```ruby + # Be sure to have the adapter's gem in your Gemfile + # and follow the adapter's specific installation + # and deployment instructions. + config.active_job.queue_adapter = :sidekiq + ``` + +* `config.active_job.default_queue_name` can be used to change the default queue name. By default this is `"default"`. + + ```ruby + config.active_job.default_queue_name = :medium_priority + ``` + +* `config.active_job.queue_name_prefix` allows you to set an optional, non-blank, queue name prefix for all jobs. By default it is blank and not used. + + The following configuration would queue the given job on the `production_high_priority` queue when run in production: + + ```ruby + config.active_job.queue_name_prefix = Rails.env + ``` + + ```ruby + class GuestsCleanupJob < ActiveJob::Base + queue_as :high_priority + #.... + end + ``` + +* `config.active_job.queue_name_delimiter` has a default value of `'_'`. If `queue_name_prefix` is set, then `queue_name_delimiter` joins the prefix and the non-prefixed queue name. + + The following configuration would queue the provided job on the `video_server.low_priority` queue: + + ```ruby + # prefix must be set for delimiter to be used + config.active_job.queue_name_prefix = 'video_server' + config.active_job.queue_name_delimiter = '.' + ``` + + ```ruby + class EncoderJob < ActiveJob::Base + queue_as :low_priority + #.... + end + ``` + +* `config.active_job.logger` accepts a logger conforming to the interface of Log4r or the default Ruby Logger class, which is then used to log information from Active Job. You can retrieve this logger by calling `logger` on either an Active Job class or an Active Job instance. Set to `nil` to disable logging. ### Configuring a Database @@ -821,15 +894,6 @@ server { Be sure to read the [NGINX documentation](http://nginx.org/en/docs/) for the most up-to-date information. -#### Considerations when deploying to a subdirectory - -Deploying to a subdirectory in production has implications on various parts of -Rails. - -* development environment: -* testing environment: -* serving static assets: -* asset pipeline: Rails Environment Settings -------------------------- @@ -959,6 +1023,11 @@ Below is a comprehensive list of all the initializers found in Rails in the orde * `active_record.set_dispatch_hooks` Resets all reloadable connections to the database if `config.cache_classes` is set to `false`. +* `active_job.logger` Sets `ActiveJob::Base.logger` - if it's not already set - + to `Rails.logger`. + +* `active_job.set_configs` Sets up Active Job by using the settings in `config.active_job` by `send`'ing the method names as setters to `ActiveJob::Base` and passing the values through. + * `action_mailer.logger` Sets `ActionMailer::Base.logger` - if it's not already set - to `Rails.logger`. * `action_mailer.set_configs` Sets up Action Mailer by using the settings in `config.action_mailer` by `send`'ing the method names as setters to `ActionMailer::Base` and passing the values through. diff --git a/guides/source/contributing_to_ruby_on_rails.md b/guides/source/contributing_to_ruby_on_rails.md index 89218f02c7..3d5f8906ca 100644 --- a/guides/source/contributing_to_ruby_on_rails.md +++ b/guides/source/contributing_to_ruby_on_rails.md @@ -32,15 +32,19 @@ Your issue report should contain a title and a clear description of the issue at Then, don't get your hopes up! Unless you have a "Code Red, Mission Critical, the World is Coming to an End" kind of bug, you're creating this issue report in the hope that others with the same problem will be able to collaborate with you on solving it. Do not expect that the issue report will automatically see any activity or that others will jump to fix it. Creating an issue like this is mostly to help yourself start on the path of fixing the problem and for others to confirm it with an "I'm having this problem too" comment. -### Create a Self-Contained gist for Active Record and Action Controller Issues +### Create an Executable Test Case -If you are filing a bug report, please use -[Active Record template for gems](https://github.com/rails/rails/blob/master/guides/bug_report_templates/active_record_gem.rb) or -[Action Controller template for gems](https://github.com/rails/rails/blob/master/guides/bug_report_templates/action_controller_gem.rb) -if the bug is found in a published gem, and -[Active Record template for master](https://github.com/rails/rails/blob/master/guides/bug_report_templates/active_record_master.rb) or -[Action Controller template for master](https://github.com/rails/rails/blob/master/guides/bug_report_templates/action_controller_master.rb) -if the bug happens in the master branch. +Having a way to reproduce your issue will be very helpful for others to help confirm, investigate and ultimately fix your issue. You can do this by providing an executable test case. To make this process easier, we have prepared several bug report templates for you to use as a starting point: + +* Template for Active Record (models, database) issues: [gem](https://github.com/rails/rails/blob/master/guides/bug_report_templates/active_record_gem.rb) / [master](https://github.com/rails/rails/blob/master/guides/bug_report_templates/active_record_master.rb) +* Template for Action Pack (controllers, routing) issues: [gem](https://github.com/rails/rails/blob/master/guides/bug_report_templates/action_controller_gem.rb) / [master](https://github.com/rails/rails/blob/master/guides/bug_report_templates/action_controller_master.rb) +* Generic template for other issues: [gem](https://github.com/rails/rails/blob/master/guides/bug_report_templates/generic_gem.rb) / [master](https://github.com/rails/rails/blob/master/guides/bug_report_templates/generic_master.rb) + +These templates include the boilerplate code to set up a test case against either a released version of Rails (`*_gem.rb`) or edge Rails (`*_master.rb`). + +Simply copy the content of the appropriate template into a `.rb` file and make the necessary changes to demonstrate the issue. You can execute it by running `ruby the_file.rb` in your terminal. If all goes well, you should see your test case failing. + +You can then share your executable test case as a [gist](https://gist.github.com), or simply paste the content into the issue description. ### Special Treatment for Security Issues @@ -355,9 +359,9 @@ $ RUBYOPT=-W0 bundle exec rake test The CHANGELOG is an important part of every release. It keeps the list of changes for every Rails version. -You should add an entry to the CHANGELOG of the framework that you modified if you're adding or removing a feature, committing a bug fix or adding deprecation notices. Refactorings and documentation changes generally should not go to the CHANGELOG. +You should add an entry **to the top** of the CHANGELOG of the framework that you modified if you're adding or removing a feature, committing a bug fix or adding deprecation notices. Refactorings and documentation changes generally should not go to the CHANGELOG. -A CHANGELOG entry should summarize what was changed and should end with author's name and it should go on top of a CHANGELOG. You can use multiple lines if you need more space and you can attach code examples indented with 4 spaces. If a change is related to a specific issue, you should attach the issue's number. Here is an example CHANGELOG entry: +A CHANGELOG entry should summarize what was changed and should end with the author's name. You can use multiple lines if you need more space and you can attach code examples indented with 4 spaces. If a change is related to a specific issue, you should attach the issue's number. Here is an example CHANGELOG entry: ``` * Summary of a change that briefly describes what was changed. You can use multiple @@ -374,11 +378,12 @@ A CHANGELOG entry should summarize what was changed and should end with author's *Your Name* ``` -Your name can be added directly after the last word if you don't provide any code examples or don't need multiple paragraphs. Otherwise, it's best to make as a new paragraph. +Your name can be added directly after the last word if there are no code +examples or multiple paragraphs. Otherwise, it's best to make a new paragraph. ### Updating the Gemfile.lock -Some changes requires the dependencies to be upgraded. In these cases make sure you run `bundle update` to get the right version of the dependency and commit the `Gemfile.lock` file within your changes. +Some changes require the dependencies to be upgraded. In these cases make sure you run `bundle update` to get the right version of the dependency and commit the `Gemfile.lock` file within your changes. ### Sanity Check @@ -398,21 +403,27 @@ When you're happy with the code on your computer, you need to commit the changes $ git commit -a ``` -At this point, your editor should be fired up and you can write a message for this commit. Well formatted and descriptive commit messages are extremely helpful for the others, especially when figuring out why given change was made, so please take the time to write it. +This should fire up your editor to write a commit message. When you have +finished, save and close to continue. + +A well-formatted and descriptive commit message is very helpful to others for +understanding why the change was made, so please take the time to write it. -Good commit message should be formatted according to the following example: +A good commit message looks like this: ``` Short summary (ideally 50 characters or less) -More detailed description, if necessary. It should be wrapped to 72 -characters. Try to be as descriptive as you can, even if you think that -the commit content is obvious, it may not be obvious to others. You -should add such description also if it's already present in bug tracker, -it should not be necessary to visit a webpage to check the history. +More detailed description, if necessary. It should be wrapped to +72 characters. Try to be as descriptive as you can. Even if you +think that the commit content is obvious, it may not be obvious +to others. Add any description that is already present in the +relevant issues; it should not be necessary to visit a webpage +to check the history. + +The description section can have multiple paragraphs. -Description can have multiple paragraphs and you can use code examples -inside, just indent it with 4 spaces: +Code examples can be embedded by indenting them with 4 spaces: class ArticlesController def index @@ -422,13 +433,15 @@ inside, just indent it with 4 spaces: You can also add bullet points: -- you can use dashes or asterisks +- make a bullet point by starting a line with either a dash (-) + or an asterisk (*) -- also, try to indent next line of a point for readability, if it's too - long to fit in 72 characters +- wrap lines at 72 characters, and indent any additional lines + with 2 spaces for readability ``` -TIP. Please squash your commits into a single commit when appropriate. This simplifies future cherry picks, and also keeps the git log clean. +TIP. Please squash your commits into a single commit when appropriate. This +simplifies future cherry picks and keeps the git log clean. ### Update Your Branch diff --git a/guides/source/debugging_rails_applications.md b/guides/source/debugging_rails_applications.md index a9715fb837..c6863f68e6 100644 --- a/guides/source/debugging_rails_applications.md +++ b/guides/source/debugging_rails_applications.md @@ -129,9 +129,14 @@ TIP: By default, each log is created under `Rails.root/log/` and the log file is ### Log Levels -When something is logged it's printed into the corresponding log if the log level of the message is equal or higher than the configured log level. If you want to know the current log level you can call the `Rails.logger.level` method. - -The available log levels are: `:debug`, `:info`, `:warn`, `:error`, `:fatal`, and `:unknown`, corresponding to the log level numbers from 0 up to 5 respectively. To change the default log level, use +When something is logged, it's printed into the corresponding log if the log +level of the message is equal or higher than the configured log level. If you +want to know the current log level, you can call the `Rails.logger.level` +method. + +The available log levels are: `:debug`, `:info`, `:warn`, `:error`, `:fatal`, +and `:unknown`, corresponding to the log level numbers from 0 up to 5, +respectively. To change the default log level, use ```ruby config.log_level = :warn # In any environment initializer, or diff --git a/guides/source/engines.md b/guides/source/engines.md index 84017d5e13..bcb0ee7d5d 100644 --- a/guides/source/engines.md +++ b/guides/source/engines.md @@ -368,7 +368,7 @@ called `Blorgh::ArticlesController` (at `app/controllers/blorgh/articles_controller.rb`) and its related views at `app/views/blorgh/articles`. This generator also generates a test for the controller (`test/controllers/blorgh/articles_controller_test.rb`) and a helper -(`app/helpers/blorgh/articles_controller.rb`). +(`app/helpers/blorgh/articles_helper.rb`). Everything this generator has created is neatly namespaced. The controller's class is defined within the `Blorgh` module: @@ -402,15 +402,6 @@ Finally, the assets for this resource are generated in two files: `app/assets/stylesheets/blorgh/articles.css`. You'll see how to use these a little later. -By default, the scaffold styling is not applied to the engine because the -engine's layout file, `app/views/layouts/blorgh/application.html.erb`, doesn't -load it. To make the scaffold styling apply, insert this line into the `<head>` -tag of this layout: - -```erb -<%= stylesheet_link_tag "scaffold" %> -``` - You can see what the engine has so far by running `rake db:migrate` at the root of our engine to run the migration generated by the scaffold generator, and then running `rails server` in `test/dummy`. When you open @@ -831,11 +822,9 @@ Notice that only _one_ migration was copied over here. This is because the first two migrations were copied over the first time this command was run. ``` -NOTE Migration [timestamp]_create_blorgh_articles.rb from blorgh has been -skipped. Migration with the same name already exists. NOTE Migration -[timestamp]_create_blorgh_comments.rb from blorgh has been skipped. Migration -with the same name already exists. Copied migration -[timestamp]_add_author_id_to_blorgh_articles.rb from blorgh +NOTE Migration [timestamp]_create_blorgh_articles.rb from blorgh has been skipped. Migration with the same name already exists. +NOTE Migration [timestamp]_create_blorgh_comments.rb from blorgh has been skipped. Migration with the same name already exists. +Copied migration [timestamp]_add_author_id_to_blorgh_articles.rb from blorgh ``` Run the migration using: @@ -1201,7 +1190,7 @@ end ``` ```ruby -# Blorgh/lib/concerns/models/article +# Blorgh/lib/concerns/models/article.rb module Blorgh::Concerns::Models::Article extend ActiveSupport::Concern diff --git a/guides/source/form_helpers.md b/guides/source/form_helpers.md index c21a2ba613..853227e2a1 100644 --- a/guides/source/form_helpers.md +++ b/guides/source/form_helpers.md @@ -241,7 +241,7 @@ Upon form submission the value entered by the user will be stored in `params[:pe WARNING: You must pass the name of an instance variable, i.e. `:person` or `"person"`, not an actual instance of your model object. -Rails provides helpers for displaying the validation errors associated with a model object. These are covered in detail by the [Active Record Validations](./active_record_validations.html#displaying-validation-errors-in-views) guide. +Rails provides helpers for displaying the validation errors associated with a model object. These are covered in detail by the [Active Record Validations](active_record_validations.html#displaying-validation-errors-in-views) guide. ### Binding a Form to an Object @@ -282,7 +282,7 @@ The resulting HTML is: </form> ``` -The name passed to `form_for` controls the key used in `params` to access the form's values. Here the name is `article` and so all the inputs have names of the form `article[attribute_name]`. Accordingly, in the `create` action `params[:article]` will be a hash with keys `:title` and `:body`. You can read more about the significance of input names in the parameter_names section. +The name passed to `form_for` controls the key used in `params` to access the form's values. Here the name is `article` and so all the inputs have names of the form `article[attribute_name]`. Accordingly, in the `create` action `params[:article]` will be a hash with keys `:title` and `:body`. You can read more about the significance of input names in the [parameter_names section](#understanding-parameter-naming-conventions). The helper methods called on the form builder are identical to the model object helpers except that it is not necessary to specify which object is being edited since this is already managed by the form builder. diff --git a/guides/source/getting_started.md b/guides/source/getting_started.md index 66cfb72aaf..684a53e472 100644 --- a/guides/source/getting_started.md +++ b/guides/source/getting_started.md @@ -23,7 +23,7 @@ application from scratch. It does not assume that you have any prior experience with Rails. However, to get the most out of it, you need to have some prerequisites installed: -* The [Ruby](https://www.ruby-lang.org/en/downloads) language version 1.9.3 or newer. +* The [Ruby](https://www.ruby-lang.org/en/downloads) language version 2.2.2 or newer. * The [RubyGems](https://rubygems.org) packaging system, which is installed with Ruby versions 1.9 and later. To learn more about RubyGems, please read the [RubyGems Guides](http://guides.rubygems.org). * A working installation of the [SQLite3 Database](https://www.sqlite.org). @@ -50,7 +50,7 @@ code while accomplishing more than many other languages and frameworks. Experienced Rails developers also report that it makes web application development more fun. -Rails is opinionated software. It makes the assumption that there is the "best" +Rails is opinionated software. It makes the assumption that there is a "best" way to do things, and it's designed to encourage that way - and in some cases to discourage alternatives. If you learn "The Rails Way" you'll probably discover a tremendous increase in productivity. If you persist in bringing old habits from @@ -97,7 +97,7 @@ For more installation methods for most Operating Systems take a look at ```bash $ ruby -v -ruby 2.0.0p353 +ruby 2.2.2p95 ``` Many popular UNIX-like OSes ship with an acceptable version of SQLite3. @@ -204,7 +204,7 @@ 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 -all the supported runtimes at [ExecJS](https://github.com/sstephenson/execjs#readme). +all the supported runtimes at [ExecJS](https://github.com/rails/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 @@ -321,9 +321,9 @@ root 'welcome#index' application to the welcome controller's index action and `get 'welcome/index'` tells Rails to map requests to <http://localhost:3000/welcome/index> to the welcome controller's index action. This was created earlier when you ran the -controller generator (`rails generate controller welcome index`). +controller generator (`bin/rails generate controller welcome index`). -Launch the web server again if you stopped it to generate the controller (`rails +Launch the web server again if you stopped it to generate the controller (`bin/rails server`) and navigate to <http://localhost:3000> in your browser. You'll see the "Hello, Rails!" message you put into `app/views/welcome/index.html.erb`, indicating that this new route is indeed going to `WelcomeController`'s `index` @@ -356,7 +356,7 @@ Rails.application.routes.draw do end ``` -If you run `rake routes`, you'll see that it has defined routes for all the +If you run `bin/rake routes`, you'll see that it has defined routes for all the standard RESTful actions. The meaning of the prefix column (and other columns) will be seen later, but for now notice that Rails has inferred the singular form `article` and makes meaningful use of the distinction. @@ -556,7 +556,7 @@ this: In this example, the `articles_path` helper is passed to the `:url` option. To see what Rails will do with this, we look back at the output of -`rake routes`: +`bin/rake routes`: ```bash $ bin/rake routes @@ -666,7 +666,7 @@ models, as that will be done automatically by Active Record. ### Running a Migration -As we've just seen, `rails generate model` created a _database migration_ file +As we've just seen, `bin/rails generate model` created a _database migration_ file inside the `db/migrate` directory. Migrations are Ruby classes that are designed to make it simple to create and modify database tables. Rails uses rake commands to run migrations, and it's possible to undo a migration after @@ -719,7 +719,7 @@ NOTE. Because you're working in the development environment by default, this command will apply to the database defined in the `development` section of your `config/database.yml` file. If you would like to execute migrations in another environment, for instance in production, you must explicitly pass it when -invoking the command: `rake db:migrate RAILS_ENV=production`. +invoking the command: `bin/rake db:migrate RAILS_ENV=production`. ### Saving data in the controller @@ -806,7 +806,7 @@ If you submit the form again now, Rails will complain about not finding the `show` action. That's not very useful though, so let's add the `show` action before proceeding. -As we have seen in the output of `rake routes`, the route for `show` action is +As we have seen in the output of `bin/rake routes`, the route for `show` action is as follows: ``` @@ -868,7 +868,7 @@ Visit <http://localhost:3000/articles/new> and give it a try! ### Listing all articles We still need a way to list all our articles, so let's do that. -The route for this as per output of `rake routes` is: +The route for this as per output of `bin/rake routes` is: ``` articles GET /articles(.:format) articles#index @@ -1363,7 +1363,7 @@ Then do the same for the `app/views/articles/edit.html.erb` view: We're now ready to cover the "D" part of CRUD, deleting articles from the database. Following the REST convention, the route for -deleting articles as per output of `rake routes` is: +deleting articles as per output of `bin/rake routes` is: ```ruby DELETE /articles/:id(.:format) articles#destroy @@ -1482,9 +1482,9 @@ Here we're using `link_to` in a different way. We pass the named route as the second argument, and then the options as another argument. The `method: :delete` and `data: { confirm: 'Are you sure?' }` options are used as HTML5 attributes so that when the link is clicked, Rails will first show a confirm dialog to the -user, and then submit the link with method `delete`. This is done via the +user, and then submit the link with method `delete`. This is done via the JavaScript file `jquery_ujs` which is automatically included in your -application's layout (`app/views/layouts/application.html.erb`) when you +application's layout (`app/views/layouts/application.html.erb`) when you generated the application. Without this file, the confirmation dialog box won't appear. @@ -1510,7 +1510,7 @@ comments on articles. We're going to see the same generator that we used before when creating the `Article` model. This time we'll create a `Comment` model to hold -reference of article comments. Run this command in your terminal: +reference to an article. Run this command in your terminal: ```bash $ bin/rails generate model Comment commenter:string body:text article:references @@ -1522,7 +1522,7 @@ This command will generate four files: | -------------------------------------------- | ------------------------------------------------------------------------------------------------------ | | db/migrate/20140120201010_create_comments.rb | Migration to create the comments table in your database (your name will include a different timestamp) | | app/models/comment.rb | The Comment model | -| test/models/comment_test.rb | Testing harness for the comments model | +| test/models/comment_test.rb | Testing harness for the comment model | | test/fixtures/comments.yml | Sample comments for use in testing | First, take a look at `app/models/comment.rb`: diff --git a/guides/source/i18n.md b/guides/source/i18n.md index 1e34261272..348c60a9d8 100644 --- a/guides/source/i18n.md +++ b/guides/source/i18n.md @@ -201,7 +201,7 @@ end If your application includes a locale switching menu, you would then have something like this in it: ```ruby -link_to("Deutsch", "#{APP_CONFIG[:deutsch_website_url]}#{request.env['REQUEST_URI']}") +link_to("Deutsch", "#{APP_CONFIG[:deutsch_website_url]}#{request.env['PATH_INFO']}") ``` assuming you would set `APP_CONFIG[:deutsch_website_url]` to some value like `http://www.application.de`. @@ -489,7 +489,9 @@ NOTE: The default locale loading mechanism in Rails does not load locale files i Overview of the I18n API Features --------------------------------- -You should have good understanding of using the i18n library now, knowing all necessary aspects of internationalizing a basic Rails application. In the following chapters, we'll cover it's features in more depth. +You should have a good understanding of using the i18n library now and know how +to internationalize a basic Rails application. In the following chapters, we'll +cover its features in more depth. These chapters will show examples using both the `I18n.translate` method as well as the [`translate` view helper method](http://api.rubyonrails.org/classes/ActionView/Helpers/TranslationHelper.html#method-i-translate) (noting the additional feature provide by the view helper method). @@ -707,7 +709,7 @@ you can safely pass the username as set by the user: ```erb <%# This is safe, it is going to be escaped if needed. %> -<%= t('welcome_html', username: @current_user.username %> +<%= t('welcome_html', username: @current_user.username) %> ``` Safe strings on the other hand are interpolated verbatim. diff --git a/guides/source/initialization.md b/guides/source/initialization.md index 8fbb234698..199545a3b3 100644 --- a/guides/source/initialization.md +++ b/guides/source/initialization.md @@ -34,7 +34,7 @@ Launch! Let's start to boot and initialize the app. A Rails application is usually started by running `rails console` or `rails server`. -### `railties/bin/rails` +### `railties/exe/rails` The `rails` in the command `rails server` is a ruby executable in your load path. This executable contains the following lines: @@ -45,7 +45,7 @@ load Gem.bin_path('railties', 'rails', version) ``` If you try out this command in a Rails console, you would see that this loads -`railties/bin/rails`. A part of the file `railties/bin/rails.rb` has the +`railties/exe/rails`. A part of the file `railties/exe/rails.rb` has the following code: ```ruby @@ -163,7 +163,7 @@ throwing an error message. If the command is valid, a method of the same name is called. ```ruby -COMMAND_WHITELIST = %(plugin generate destroy console server dbconsole application runner new version help) +COMMAND_WHITELIST = %w(plugin generate destroy console server dbconsole application runner new version help) def run_command!(command) command = parse_command(command) diff --git a/guides/source/layouts_and_rendering.md b/guides/source/layouts_and_rendering.md index c57fa358d6..737f392995 100644 --- a/guides/source/layouts_and_rendering.md +++ b/guides/source/layouts_and_rendering.md @@ -123,8 +123,6 @@ Content-Type: */*; charset=utf-8 X-Runtime: 0.014297 Set-Cookie: _blog_session=...snip...; path=/; HttpOnly Cache-Control: no-cache - -$ ``` We see there is an empty response (no data after the `Cache-Control` line), but the request was successful because Rails has set the response to 200 OK. You can set the `:status` option on render to change this response. Rendering nothing can be useful for Ajax requests where all you want to send back to the browser is an acknowledgment that the request was completed. @@ -1075,9 +1073,14 @@ One way to use partials is to treat them as the equivalent of subroutines: as a <%= render "shared/footer" %> ``` -Here, the `_ad_banner.html.erb` and `_footer.html.erb` partials could contain content that is shared among many pages in your application. You don't need to see the details of these sections when you're concentrating on a particular page. +Here, the `_ad_banner.html.erb` and `_footer.html.erb` partials could contain +content that is shared by many pages in your application. You don't need to see +the details of these sections when you're concentrating on a particular page. -As you already could see from the previous sections of this guide, `yield` is a very powerful tool for cleaning up your layouts. Keep in mind that it's pure ruby, so you can use it almost everywhere. For example, we can use it to DRY form layout definition for several similar resources: +As seen in the previous sections of this guide, `yield` is a very powerful tool +for cleaning up your layouts. Keep in mind that it's pure Ruby, so you can use +it almost everywhere. For example, we can use it to DRY up form layout +definitions for several similar resources: * `users/index.html.erb` diff --git a/guides/source/plugins.md b/guides/source/plugins.md index 10738320ef..4e630a39f3 100644 --- a/guides/source/plugins.md +++ b/guides/source/plugins.md @@ -265,7 +265,7 @@ module Yaffle end end -ActiveRecord::Base.send :include, Yaffle::ActsAsYaffle +ActiveRecord::Base.include(Yaffle::ActsAsYaffle) ``` You can then return to the root directory (`cd ../..`) of your plugin and rerun the tests using `rake`. @@ -308,7 +308,7 @@ module Yaffle end end -ActiveRecord::Base.send :include, Yaffle::ActsAsYaffle +ActiveRecord::Base.include(Yaffle::ActsAsYaffle) ``` When you run `rake`, you should see the tests all pass: @@ -382,7 +382,7 @@ module Yaffle end end -ActiveRecord::Base.send :include, Yaffle::ActsAsYaffle +ActiveRecord::Base.include(Yaffle::ActsAsYaffle) ``` Run `rake` one final time and you should see: diff --git a/guides/source/routing.md b/guides/source/routing.md index b5defc9d20..4ccc50a4d9 100644 --- a/guides/source/routing.md +++ b/guides/source/routing.md @@ -807,6 +807,18 @@ As long as `Sprockets` responds to `call` and returns a `[status, headers, body] NOTE: For the curious, `'articles#index'` actually expands out to `ArticlesController.action(:index)`, which returns a valid Rack application. +If you specify a rack application as the endpoint for a matcher remember that the route will be unchanged in the receiving application. With the following route your rack application should expect the route to be '/admin': + +```ruby +match '/admin', to: AdminApp, via: :all +``` + +If you would prefer to have your rack application receive requests at the root path instead use mount: + +```ruby +mount AdminApp, at: '/admin' +``` + ### Using `root` You can specify what Rails should route `'/'` to with the `root` method: @@ -909,7 +921,7 @@ The `:as` option lets you override the normal naming for the named route helpers resources :photos, as: 'images' ``` -will recognize incoming paths beginning with `/photos` and route the requests to `PhotosController`, but use the value of the :as option to name the helpers. +will recognize incoming paths beginning with `/photos` and route the requests to `PhotosController`, but use the value of the `:as` option to name the helpers. | HTTP Verb | Path | Controller#Action | Named Helper | | --------- | ---------------- | ----------------- | -------------------- | @@ -1006,7 +1018,7 @@ TIP: If your application has many RESTful routes, using `:only` and `:except` to ### Translated Paths -Using `scope`, we can alter path names generated by resources: +Using `scope`, we can alter path names generated by `resources`: ```ruby scope(path_names: { new: 'neu', edit: 'bearbeiten' }) do diff --git a/guides/source/security.md b/guides/source/security.md index e486edde31..91d520e997 100644 --- a/guides/source/security.md +++ b/guides/source/security.md @@ -449,14 +449,16 @@ Depending on your web application, there may be more ways to hijack the user's a ### CAPTCHAs -INFO: _A CAPTCHA is a challenge-response test to determine that the response is not generated by a computer. It is often used to protect comment forms from automatic spam bots by asking the user to type the letters of a distorted image. The idea of a negative CAPTCHA is not for a user to prove that they are human, but reveal that a robot is a robot._ +INFO: _A CAPTCHA is a challenge-response test to determine that the response is not generated by a computer. It is often used to protect registration forms from attackers and comment forms from automatic spam bots by asking the user to type the letters of a distorted image. This is the positive CAPTCHA, but there is also the negative CAPTCHA. The idea of a negative CAPTCHA is not for a user to prove that they are human, but reveal that a robot is a robot._ -But not only spam robots (bots) are a problem, but also automatic login bots. A popular CAPTCHA API is [reCAPTCHA](http://recaptcha.net/) which displays two distorted images of words from old books. It also adds an angled line, rather than a distorted background and high levels of warping on the text as earlier CAPTCHAs did, because the latter were broken. As a bonus, using reCAPTCHA helps to digitize old books. [ReCAPTCHA](https://github.com/ambethia/recaptcha/) is also a Rails plug-in with the same name as the API. +A popular positive CAPTCHA API is [reCAPTCHA](http://recaptcha.net/) which displays two distorted images of words from old books. It also adds an angled line, rather than a distorted background and high levels of warping on the text as earlier CAPTCHAs did, because the latter were broken. As a bonus, using reCAPTCHA helps to digitize old books. [ReCAPTCHA](https://github.com/ambethia/recaptcha/) is also a Rails plug-in with the same name as the API. You will get two keys from the API, a public and a private key, which you have to put into your Rails environment. After that you can use the recaptcha_tags method in the view, and the verify_recaptcha method in the controller. Verify_recaptcha will return false if the validation fails. -The problem with CAPTCHAs is, they are annoying. Additionally, some visually impaired users have found certain kinds of distorted CAPTCHAs difficult to read. The idea of negative CAPTCHAs is not to ask a user to proof that they are human, but reveal that a spam robot is a bot. +The problem with CAPTCHAs is that they have a negative impact on the user experience. Additionally, some visually impaired users have found certain kinds of distorted CAPTCHAs difficult to read. Still, positive CAPTCHAs are one of the best methods to prevent all kinds of bots from submitting forms. -Most bots are really dumb, they crawl the web and put their spam into every form's field they can find. Negative CAPTCHAs take advantage of that and include a "honeypot" field in the form which will be hidden from the human user by CSS or JavaScript. +Most bots are really dumb. They crawl the web and put their spam into every form's field they can find. Negative CAPTCHAs take advantage of that and include a "honeypot" field in the form which will be hidden from the human user by CSS or JavaScript. + +Note that negative CAPTCHAs are only effective against dumb bots and won't suffice to protect critical applications from targeted bots. Still, the negative and positive CAPTCHAs can be combined to increase the performance, e.g., if the "honeypot" field is not empty (bot detected), you won't need to verify the positive CAPTCHA, which would require a HTTPS request to Google ReCaptcha before computing the response. Here are some ideas how to hide honeypot fields by JavaScript and/or CSS: @@ -710,7 +712,7 @@ The log files on www.attacker.com will read like this: GET http://www.attacker.com/_app_session=836c1c25278e5b321d6bea4f19cb57e2 ``` -You can mitigate these attacks (in the obvious way) by adding the [httpOnly](http://dev.rubyonrails.org/ticket/8895) flag to cookies, so that document.cookie may not be read by JavaScript. Http only cookies can be used from IE v6.SP1, Firefox v2.0.0.5 and Opera 9.5. Safari is still considering, it ignores the option. But other, older browsers (such as WebTV and IE 5.5 on Mac) can actually cause the page to fail to load. Be warned that cookies [will still be visible using Ajax](http://ha.ckers.org/blog/20070719/firefox-implements-httponly-and-is-vulnerable-to-xmlhttprequest/), though. +You can mitigate these attacks (in the obvious way) by adding the **httpOnly** flag to cookies, so that document.cookie may not be read by JavaScript. Http only cookies can be used from IE v6.SP1, Firefox v2.0.0.5 and Opera 9.5. Safari is still considering, it ignores the option. But other, older browsers (such as WebTV and IE 5.5 on Mac) can actually cause the page to fail to load. Be warned that cookies [will still be visible using Ajax](http://ha.ckers.org/blog/20070719/firefox-implements-httponly-and-is-vulnerable-to-xmlhttprequest/), though. ##### Defacement diff --git a/guides/source/testing.md b/guides/source/testing.md index cb3bd68fbe..f12daf0dbc 100644 --- a/guides/source/testing.md +++ b/guides/source/testing.md @@ -59,9 +59,9 @@ You can find comprehensive documentation in the [Fixtures API documentation](htt #### What Are Fixtures? -_Fixtures_ is a fancy word for sample data. Fixtures allow you to populate your testing database with predefined data before your tests run. Fixtures are database independent written in YAML. There is one file per model. +_Fixtures_ is a fancy word for sample data. Fixtures allow you to populate your testing database with predefined data before your tests run. Fixtures are database independent and written in YAML. There is one file per model. -You'll find fixtures under your `test/fixtures` directory. When you run `rails generate model` to create a new model fixture stubs will be automatically created and placed in this directory. +You'll find fixtures under your `test/fixtures` directory. When you run `rails generate model` to create a new model, Rails automatically creates fixture stubs in this directory. #### YAML @@ -82,7 +82,7 @@ steve: profession: guy with keyboard ``` -Each fixture is given a name followed by an indented list of colon-separated key/value pairs. Records are typically separated by a blank space. You can place comments in a fixture file by using the # character in the first column. +Each fixture is given a name followed by an indented list of colon-separated key/value pairs. Records are typically separated by a blank line. You can place comments in a fixture file by using the # character in the first column. If you are working with [associations](/association_basics.html), you can simply define a reference node between two different fixtures. Here's an example with @@ -138,35 +138,26 @@ users(:david) users(:david).id # one can also access methods available on the User class -email(david.girlfriend.email, david.location_tonight) +email(david.partner.email, david.location_tonight) ``` -### Rake Tasks for Running your Tests +### Console Tasks for Running your Tests -Rails comes with a number of built-in rake tasks to help with testing. The -table below lists the commands included in the default Rakefile when a Rails -project is created. +Rails comes with a CLI command to run tests. +Here are some examples of how to use it: -| Tasks | Description | -| ----------------------- | ----------- | -| `rake test` | Runs all tests in the `test` directory. You can also run `rake` and Rails will run all tests by default | -| `rake test:controllers` | Runs all the controller tests from `test/controllers` | -| `rake test:functionals` | Runs all the functional tests from `test/controllers`, `test/mailers`, and `test/functional` | -| `rake test:helpers` | Runs all the helper tests from `test/helpers` | -| `rake test:integration` | Runs all the integration tests from `test/integration` | -| `rake test:jobs` | Runs all the job tests from `test/jobs` | -| `rake test:mailers` | Runs all the mailer tests from `test/mailers` | -| `rake test:models` | Runs all the model tests from `test/models` | -| `rake test:units` | Runs all the unit tests from `test/models`, `test/helpers`, and `test/unit` | -| `rake test:db` | Runs all tests in the `test` directory and resets the db | +```bash +$ bin/rails test # run all tests in the `test` directory +$ bin/rails test test/controllers # run all tests from specific directory +$ bin/rails test test/models/post_test.rb # run specific test +$ bin/rails test test/models/post_test.rb:44 # run specific test and line +``` We will cover each of types Rails tests listed above in this guide. -Unit Testing your Models +Model Testing ------------------------ -In Rails, unit tests are what you write to test your models. - For this guide we will be using the application we built in the [Getting Started with Rails](getting_started.html) guide. If you remember when you used the `rails generate scaffold` command from earlier. We created our first resource among other things it created a test stub in the `test/models` directory: @@ -259,10 +250,10 @@ be rebuilt. This can be done by executing `bin/rake db:test:prepare`. ### Running Tests -Running a test is as simple as invoking the file containing the test cases through `rake test` command. +Running a test is as simple as invoking the file containing the test cases through `rails test` command. ```bash -$ bin/rake test test/models/article_test.rb +$ bin/rails test test/models/article_test.rb . Finished tests in 0.009262s, 107.9680 tests/s, 107.9680 assertions/s. @@ -275,7 +266,7 @@ This will run all test methods from the test case. You can also run a particular test method from the test case by running the test and providing the `test method name`. ```bash -$ bin/rake test test/models/article_test.rb test_the_truth +$ bin/rails test test/models/article_test.rb test_the_truth . Finished tests in 0.009064s, 110.3266 tests/s, 110.3266 assertions/s. @@ -296,10 +287,10 @@ test "should not save article without title" do end ``` -Let us run this newly added test. +Let us run this newly added test (where `6` is the number of line where the test is defined). ```bash -$ bin/rake test test/models/article_test.rb test_should_not_save_article_without_title +$ bin/rails test test/models/article_test.rb:6 F Finished tests in 0.044632s, 22.4054 tests/s, 22.4054 assertions/s. @@ -339,7 +330,7 @@ end Now the test should pass. Let us verify by running the test again: ```bash -$ bin/rake test test/models/article_test.rb test_should_not_save_article_without_title +$ bin/rails test test/models/article_test.rb:6 . Finished tests in 0.047721s, 20.9551 tests/s, 20.9551 assertions/s. @@ -368,7 +359,7 @@ end Now you can see even more output in the console from running the tests: ```bash -$ bin/rake test test/models/article_test.rb test_should_report_error +$ bin/rails test test/models/article_test.rb E Finished tests in 0.030974s, 32.2851 tests/s, 0.0000 assertions/s. @@ -393,11 +384,10 @@ When a test fails you are presented with the corresponding backtrace. By default Rails filters that backtrace and will only print lines relevant to your application. This eliminates the framework noise and helps to focus on your code. However there are situations when you want to see the full -backtrace. simply set the `BACKTRACE` environment variable to enable this -behavior: +backtrace. Simply set the `-b` (or `--backtrace`) argument to enable this behavior: ```bash -$ BACKTRACE=1 bin/rake test test/models/article_test.rb +$ bin/rails test -b test/models/article_test.rb ``` If we want this test to pass we can modify it to use `assert_raises` like so: @@ -504,13 +494,13 @@ All the keyword arguments are optional. Example: Calling the `:show` action, passing an `id` of 12 as the `params` and setting a `user_id` of 5 in the session: ```ruby -get(:show, params: { 'id' => "12" }, session: { 'user_id' => 5 }) +get(:show, params: { id: 12 }, session: { user_id: 5 }) ``` Another example: Calling the `:view` action, passing an `id` of 12 as the `params`, this time with no session, but with a flash message. ```ruby -get(:view, params: { 'id' => '12' }, flash: { 'message' => 'booya!' }) +get(:view, params: { id: 12 }, flash: { message: 'booya!' }) ``` NOTE: If you try running `test_should_create_article` test from `articles_controller_test.rb` it will fail on account of the newly added model level validation and rightly so. @@ -632,7 +622,7 @@ WARNING: You must include the "layouts" directory name even if you save your lay If your view renders any partial, when asserting for the layout, you can to assert for the partial at the same time. Otherwise, assertion will fail. -Remember, we added the "_form" partial to our creating Articles view? Let's write an assertion for that in the `:new` action now: +Remember, we added the "_form" partial to our new Article view? Let's write an assertion for that in the `:new` action now: ```ruby test "new should render correct layout" do @@ -666,7 +656,7 @@ end If we run our test now, we should see a failure: ```bash -$ bin/rake test test/controllers/articles_controller_test.rb test_should_create_article +$ bin/rails test test/controllers/articles_controller_test.rb test_should_create_article Run options: -n test_should_create_article --seed 32266 # Running: @@ -704,7 +694,7 @@ end Now if we run our tests, we should see it pass: ```bash -$ bin/rake test test/controllers/articles_controller_test.rb test_should_create_article +$ bin/rails test test/controllers/articles_controller_test.rb test_should_create_article Run options: -n test_should_create_article --seed 18981 # Running: @@ -852,7 +842,7 @@ end I've added this file here `test/controllers/articles_routes_test.rb` and if we run the test we should see: ```bash -$ bin/rake test test/controllers/articles_routes_test.rb +$ bin/rails test test/controllers/articles_routes_test.rb # Running: @@ -929,7 +919,7 @@ assert_select_email do end ``` -Testing helpers +Testing Helpers --------------- In order to test helpers, all you need to do is check that the output of the @@ -1083,7 +1073,7 @@ Testing mailer classes requires some specific tools to do a thorough job. ### Keeping the Postman in Check -Your mailer classes - like every other part of your Rails application - should be tested to ensure that it is working as expected. +Your mailer classes - like every other part of your Rails application - should be tested to ensure that they are working as expected. The goals of testing your mailer classes are to ensure that: diff --git a/guides/source/upgrading_ruby_on_rails.md b/guides/source/upgrading_ruby_on_rails.md index 20b90bdba0..0aa2152d56 100644 --- a/guides/source/upgrading_ruby_on_rails.md +++ b/guides/source/upgrading_ruby_on_rails.md @@ -20,7 +20,7 @@ The best way to be sure that your application still works after upgrading is to Rails generally stays close to the latest released Ruby version when it's released: -* Rails 5 requires Ruby 2.2 or newer. +* Rails 5 requires Ruby 2.2.2 or newer. * Rails 4 prefers Ruby 2.0 and requires 1.9.3 or newer. * Rails 3.2.x is the last branch to support Ruby 1.8.7. * Rails 3 and above require Ruby 1.8.7 or higher. Support for all of the previous Ruby versions has been dropped officially. You should upgrade as early as possible. @@ -75,6 +75,23 @@ warning by adding the following configuration to your `config/application.rb`: See [#17227](https://github.com/rails/rails/pull/17227) for more details. +### ActiveJob jobs now inherent from ApplicationJob by default + +In Rails 4.2 an ActiveJob inherits from `ActiveJob::Base`. In Rails 5.0 this +behavior has changed to now inherit from `ApplicationJob`. + +When upgrading from Rails 4.2 to Rails 5.0 you need to create an +`application_job.rb` file in `app/jobs/` and add the following content: + +``` +class ApplicationJob < ActiveJob::Base +end +``` + +Then make sure that all your job classes inherit from it. + +See [#19034](https://github.com/rails/rails/pull/19034) for more details. + Upgrading from Rails 4.1 to Rails 4.2 ------------------------------------- @@ -281,7 +298,7 @@ end The migration DSL has been expanded to support foreign key definitions. If you've been using the Foreigner gem, you might want to consider removing it. Note that the foreign key support of Rails is a subset of Foreigner. This means -that not every Foreigner definition can be fully replaced by it's Rails +that not every Foreigner definition can be fully replaced by its Rails migration DSL counterpart. The migration procedure is as follows: diff --git a/rails.gemspec b/rails.gemspec index 1398169922..52112680b2 100644 --- a/rails.gemspec +++ b/rails.gemspec @@ -7,7 +7,7 @@ Gem::Specification.new do |s| s.summary = 'Full-stack web application framework.' s.description = 'Ruby on Rails is a full-stack web framework optimized for programmer happiness and sustainable productivity. It encourages beautiful code by favoring convention over configuration.' - s.required_ruby_version = '>= 2.2.0' + s.required_ruby_version = '>= 2.2.2' s.required_rubygems_version = '>= 1.8.11' s.license = 'MIT' diff --git a/railties/CHANGELOG.md b/railties/CHANGELOG.md index 8b10144413..43eee9f442 100644 --- a/railties/CHANGELOG.md +++ b/railties/CHANGELOG.md @@ -1,22 +1,52 @@ -* Created rake restart task. Restarts your Rails app by touching the - `tmp/restart.txt`. +* Remove sqlite support from `rails dbconsole`. - Fixes #18876. + *Andrew White* - *Hyonjee Joo* +* Rename `railties/bin` to `railties/exe` to match the new Bundler executables + convention. + + *Islam Wazery* + +* Print `bundle install` output in `rails new` as soon as it's available. + + Running `rails new` will now print the output of `bundle install` as + it is available, instead of waiting until all gems finish installing. + + *Max Holder* + +* Respect `pluralize_table_names` when generating fixture file. + + Fixes #19519. + + *Yuji Yaginuma* -* Set Rails console to use log formatter and log level as specified for the - given environment. +* Add a new-line to the end of route method generated code. - Fixes #15470. + We need to add a `\n`, because we cannot have two routes + in the same line. - *Jacob Evelyn* + *arthurnn* -* Add `config/initializers/active_record_belongs_to_required_by_default.rb` +* Add `rake initializers`. + + This task prints out all defined initializers in the order they are invoked + by Rails. This is helpful for debugging issues related to the initialization + process. + + *Naoto Kaneko* + +* Created rake restart task. Restarts your Rails app by touching the + `tmp/restart.txt`. + + Fixes #18876. + + *Hyonjee Joo* + +* Add `config/initializers/active_record_belongs_to_required_by_default.rb`. Newly generated Rails apps have a new initializer called `active_record_belongs_to_required_by_default.rb` which sets the value of - the configuration option `config.active_record.belongs_to_requred_by_default` + the configuration option `config.active_record.belongs_to_required_by_default` to `true` when ActiveRecord is not skipped. As a result, new Rails apps require `belongs_to` association on model @@ -31,7 +61,7 @@ 'No such middleware' errors when `insert_before` or `insert_after` are added after the `delete` operation for the middleware being deleted. - Fixes: #16433. + Fixes #16433. *Guo Xiang Tan* @@ -43,7 +73,7 @@ *Xavier Noria* -* Force generated routes to be inserted into routes.rb +* Force generated routes to be inserted into `config/routes.rb`. *Andrew White* @@ -57,11 +87,12 @@ *Melanie Gilman* -* Add the `method_source` gem to the default Gemfile for apps +* Add the `method_source` gem to the default Gemfile for apps. *Sean Griffin* -* Drop old test locations from `rake stats` +* Drop old test locations from `rake stats`: + - test/functional - test/unit @@ -92,7 +123,7 @@ *Andrew Kozlov* -* Add `config/initializers/callback_terminator.rb` +* Add `config/initializers/callback_terminator.rb`. Newly generated Rails apps have a new initializer called `callback_terminator.rb` which sets the value of the configuration option diff --git a/railties/bin/rails b/railties/exe/rails index 82c17cabce..82c17cabce 100755 --- a/railties/bin/rails +++ b/railties/exe/rails diff --git a/railties/lib/rails/app_rails_loader.rb b/railties/lib/rails/app_rails_loader.rb index 39d8007333..9a7c6c5f2d 100644 --- a/railties/lib/rails/app_rails_loader.rb +++ b/railties/lib/rails/app_rails_loader.rb @@ -1,4 +1,5 @@ require 'pathname' +require 'rails/version' module Rails module AppRailsLoader @@ -9,7 +10,7 @@ module Rails BUNDLER_WARNING = <<EOS Looks like your app's ./bin/rails is a stub that was generated by Bundler. -In Rails 4, your app's bin/ directory contains executables that are versioned +In Rails #{Rails::VERSION::MAJOR}, your app's bin/ directory contains executables that are versioned like any other source code, rather than stubs that are generated on demand. Here's how to upgrade: diff --git a/railties/lib/rails/application.rb b/railties/lib/rails/application.rb index ff6c905f3e..a65f8f2ad9 100644 --- a/railties/lib/rails/application.rb +++ b/railties/lib/rails/application.rb @@ -1,4 +1,5 @@ require 'fileutils' +require 'yaml' require 'active_support/core_ext/hash/keys' require 'active_support/core_ext/object/blank' require 'active_support/key_generator' @@ -159,8 +160,9 @@ module Rails # Implements call according to the Rack API. It simply # dispatches the request to the underlying middleware stack. def call(env) - env["ORIGINAL_FULLPATH"] = build_original_fullpath(env) - env["ORIGINAL_SCRIPT_NAME"] = env["SCRIPT_NAME"] + req = ActionDispatch::Request.new env + env["ORIGINAL_FULLPATH"] = req.fullpath + env["ORIGINAL_SCRIPT_NAME"] = req.script_name super(env) end @@ -227,7 +229,6 @@ module Rails 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 @@ -504,25 +505,13 @@ module Rails default_stack.build_stack end - def build_original_fullpath(env) #:nodoc: - path_info = env["PATH_INFO"] - query_string = env["QUERY_STRING"] - script_name = env["SCRIPT_NAME"] - - if query_string.present? - "#{script_name}#{path_info}?#{query_string}" - else - "#{script_name}#{path_info}" - end - end - def validate_secret_key_config! #:nodoc: if secrets.secret_key_base.blank? ActiveSupport::Deprecation.warn "You didn't set `secret_key_base`. " + "Read the upgrade documentation to learn more about this new config option." if secrets.secret_token.blank? - raise "Missing `secret_token` and `secret_key_base` for '#{Rails.env}' environment, set these values in `config/secrets.yml`" + raise "Missing `secret_key_base` for '#{Rails.env}' environment, set this value in `config/secrets.yml`" end end end diff --git a/railties/lib/rails/code_statistics.rb b/railties/lib/rails/code_statistics.rb index 583d005d46..fd352dc9b7 100644 --- a/railties/lib/rails/code_statistics.rb +++ b/railties/lib/rails/code_statistics.rb @@ -41,11 +41,9 @@ class CodeStatistics #:nodoc: if File.directory?(path) && (/^\./ !~ file_name) stats.add(calculate_directory_statistics(path, pattern)) + elsif file_name =~ pattern + stats.add_by_file_path(path) end - - next unless file_name =~ pattern - - stats.add_by_file_path(path) end stats diff --git a/railties/lib/rails/commands.rb b/railties/lib/rails/commands.rb index f32bf772a5..12bd73db24 100644 --- a/railties/lib/rails/commands.rb +++ b/railties/lib/rails/commands.rb @@ -6,7 +6,8 @@ aliases = { "c" => "console", "s" => "server", "db" => "dbconsole", - "r" => "runner" + "r" => "runner", + "t" => "test", } command = ARGV.shift diff --git a/railties/lib/rails/commands/commands_tasks.rb b/railties/lib/rails/commands/commands_tasks.rb index 8bae08e44e..685d55eea8 100644 --- a/railties/lib/rails/commands/commands_tasks.rb +++ b/railties/lib/rails/commands/commands_tasks.rb @@ -14,6 +14,7 @@ The most common rails commands are: generate Generate new code (short-cut alias: "g") console Start the Rails console (short-cut alias: "c") server Start the Rails server (short-cut alias: "s") + test Run tests (short-cut alias: "t") dbconsole Start a console for the database specified in config/database.yml (short-cut alias: "db") new Create a new Rails application. "rails new my_app" creates a @@ -27,7 +28,7 @@ In addition to those, there are: All commands can be run with -h (or --help) for more information. EOT - COMMAND_WHITELIST = %w(plugin generate destroy console server dbconsole runner new version help) + COMMAND_WHITELIST = %w(plugin generate destroy console server dbconsole runner new version help test) def initialize(argv) @argv = argv @@ -81,6 +82,10 @@ EOT end end + def test + require_command!("test") + end + def dbconsole require_command!("dbconsole") Rails::DBConsole.start @@ -146,6 +151,17 @@ EOT puts HELP_MESSAGE end + # Output an error message stating that the attempted command is not a valid rails command. + # Run the attempted command as a rake command with the --dry-run flag. If successful, suggest + # to the user that they possibly meant to run the given rails command as a rake command. + # Append the help message. + # + # Example: + # $ rails db:migrate + # Error: Command 'db:migrate' not recognized + # Did you mean: `$ rake db:migrate` ? + # (Help message output) + # def write_error_message(command) puts "Error: Command '#{command}' not recognized" if %x{rake #{command} --dry-run 2>&1 } && $?.success? diff --git a/railties/lib/rails/commands/dbconsole.rb b/railties/lib/rails/commands/dbconsole.rb index 5175e31f14..3b22b582cf 100644 --- a/railties/lib/rails/commands/dbconsole.rb +++ b/railties/lib/rails/commands/dbconsole.rb @@ -50,9 +50,6 @@ module Rails ENV['PGPASSWORD'] = config["password"].to_s if config["password"] && options['include_password'] find_cmd_and_exec('psql', config["database"]) - when "sqlite" - find_cmd_and_exec('sqlite', config["database"]) - when "sqlite3" args = [] @@ -89,7 +86,7 @@ module Rails find_cmd_and_exec("sqsh", *args) else - abort "Unknown command-line client for #{config['database']}. Submit a Rails patch to add support!" + abort "Unknown command-line client for #{config['database']}." end end diff --git a/railties/lib/rails/commands/test.rb b/railties/lib/rails/commands/test.rb new file mode 100644 index 0000000000..598e224a6f --- /dev/null +++ b/railties/lib/rails/commands/test.rb @@ -0,0 +1,5 @@ +require "rails/test_unit/runner" + +$: << File.expand_path("../../test", APP_PATH) + +Rails::TestRunner.run(ARGV) diff --git a/railties/lib/rails/generators.rb b/railties/lib/rails/generators.rb index 341291f08b..a7da92168d 100644 --- a/railties/lib/rails/generators.rb +++ b/railties/lib/rails/generators.rb @@ -267,10 +267,13 @@ module Rails d = (0..m).to_a x = nil - str1.each_char.each_with_index do |char1,i| + # avoid duplicating an enumerable object in the loop + str2_codepoint_enumerable = str2.each_codepoint + + str1.each_codepoint.with_index do |char1, i| e = i+1 - str2.each_char.each_with_index do |char2,j| + str2_codepoint_enumerable.with_index do |char2, j| cost = (char1 == char2) ? 0 : 1 x = [ d[j+1] + 1, # insertion diff --git a/railties/lib/rails/generators/actions.rb b/railties/lib/rails/generators/actions.rb index c1bc646c65..70a20801a0 100644 --- a/railties/lib/rails/generators/actions.rb +++ b/railties/lib/rails/generators/actions.rb @@ -221,7 +221,7 @@ module Rails sentinel = /\.routes\.draw do\s*\n/m in_root do - inject_into_file 'config/routes.rb', " #{routing_code}", { after: sentinel, verbose: false, force: true } + inject_into_file 'config/routes.rb', " #{routing_code}\n", { after: sentinel, verbose: false, force: true } end end diff --git a/railties/lib/rails/generators/app_base.rb b/railties/lib/rails/generators/app_base.rb index 253272c7dd..119a7cb829 100644 --- a/railties/lib/rails/generators/app_base.rb +++ b/railties/lib/rails/generators/app_base.rb @@ -291,7 +291,7 @@ module Rails end def javascript_runtime_gemfile_entry - comment = 'See https://github.com/sstephenson/execjs#readme for more supported runtimes' + comment = 'See https://github.com/rails/execjs#readme for more supported runtimes' if defined?(JRUBY_VERSION) GemfileEntry.version 'therubyrhino', nil, comment else @@ -315,10 +315,6 @@ module Rails # its own vendored Thor, which could be a different version. Running both # things in the same process is a recipe for a night with paracetamol. # - # We use backticks and #print here instead of vanilla #system because it - # is easier to silence stdout in the existing test suite this way. The - # end-user gets the bundler commands called anyway, so no big deal. - # # We unset temporary bundler variables to load proper bundler and Gemfile. # # Thanks to James Tucker for the Gem tricks involved in this call. @@ -326,8 +322,12 @@ module Rails require 'bundler' Bundler.with_clean_env do - output = `"#{Gem.ruby}" "#{_bundle_command}" #{command}` - print output unless options[:quiet] + full_command = %Q["#{Gem.ruby}" "#{_bundle_command}" #{command}] + if options[:quiet] + system(full_command, out: File::NULL) + else + system(full_command) + end end end @@ -336,7 +336,7 @@ module Rails end def spring_install? - !options[:skip_spring] && Process.respond_to?(:fork) && !RUBY_PLATFORM.include?("cygwin") + !options[:skip_spring] && !options.dev? && Process.respond_to?(:fork) && !RUBY_PLATFORM.include?("cygwin") end def run_bundle 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 d9713b0238..d99b27cb2d 100644 --- a/railties/lib/rails/generators/erb/scaffold/templates/_form.html.erb +++ b/railties/lib/rails/generators/erb/scaffold/templates/_form.html.erb @@ -17,6 +17,7 @@ <%%= f.label :password %><br> <%%= f.password_field :password %> </div> + <div class="field"> <%%= f.label :password_confirmation %><br> <%%= f.password_field :password_confirmation %> @@ -25,6 +26,7 @@ <%%= f.<%= attribute.field_type %> :<%= attribute.column_name %> %> <% end -%> </div> + <% end -%> <div class="actions"> <%%= f.submit %> diff --git a/railties/lib/rails/generators/named_base.rb b/railties/lib/rails/generators/named_base.rb index 36456e64f5..01a8e2e9b4 100644 --- a/railties/lib/rails/generators/named_base.rb +++ b/railties/lib/rails/generators/named_base.rb @@ -141,6 +141,10 @@ module Rails @plural_file_name ||= file_name.pluralize end + def fixture_file_name + @fixture_file_name ||= (pluralize_table_names? ? plural_file_name : file_name) + end + def route_url @route_url ||= class_path.collect {|dname| "/" + dname }.join + "/" + plural_file_name end diff --git a/railties/lib/rails/generators/rails/app/templates/Gemfile b/railties/lib/rails/generators/rails/app/templates/Gemfile index 82a0315379..c11bb58bfa 100644 --- a/railties/lib/rails/generators/rails/app/templates/Gemfile +++ b/railties/lib/rails/generators/rails/app/templates/Gemfile @@ -36,10 +36,6 @@ group :development, :test do # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring gem 'spring' <% end -%> - - # Adds `Method#source` and `Method#comment` to get the source code of a - # method from the console - gem 'method_source' <% end -%> end <% if RUBY_PLATFORM.match(/bccwin|cygwin|emx|mingw|mswin|wince|java/) -%> diff --git a/railties/lib/rails/generators/rails/app/templates/app/assets/javascripts/application.js.tt b/railties/lib/rails/generators/rails/app/templates/app/assets/javascripts/application.js.tt index c1a77944e8..cb86978d4c 100644 --- a/railties/lib/rails/generators/rails/app/templates/app/assets/javascripts/application.js.tt +++ b/railties/lib/rails/generators/rails/app/templates/app/assets/javascripts/application.js.tt @@ -7,7 +7,7 @@ // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the // compiled file. // -// Read Sprockets README (https://github.com/sstephenson/sprockets#sprockets-directives) for details +// Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details // about supported directives. // <% unless options[:skip_javascript] -%> diff --git a/railties/lib/rails/generators/rails/app/templates/app/jobs/application_job.rb b/railties/lib/rails/generators/rails/app/templates/app/jobs/application_job.rb new file mode 100644 index 0000000000..a009ace51c --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/app/jobs/application_job.rb @@ -0,0 +1,2 @@ +class ApplicationJob < ActiveJob::Base +end diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcmysql.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcmysql.yml index acb93939e1..f5b62e8fb3 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcmysql.yml +++ b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcmysql.yml @@ -7,7 +7,7 @@ # gem 'activerecord-jdbcmysql-adapter' # # And be sure to use new-style password hashing: -# http://dev.mysql.com/doc/refman/5.0/en/old-client.html +# http://dev.mysql.com/doc/refman/5.6/en/old-client.html # default: &default adapter: mysql diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/mysql.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/mysql.yml index 596c916573..b0767bd93a 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/databases/mysql.yml +++ b/railties/lib/rails/generators/rails/app/templates/config/databases/mysql.yml @@ -7,7 +7,7 @@ # gem 'mysql2' # # And be sure to use new-style password hashing: -# http://dev.mysql.com/doc/refman/5.0/en/old-client.html +# http://dev.mysql.com/doc/refman/5.6/en/old-client.html # default: &default adapter: mysql2 diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/callback_terminator.rb b/railties/lib/rails/generators/rails/app/templates/config/initializers/callback_terminator.rb index e63022da91..a70a1b9cde 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/initializers/callback_terminator.rb +++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/callback_terminator.rb @@ -1,4 +1,4 @@ # Be sure to restart your server when you modify this file. # Do not halt callback chains when a callback returns false. -Rails.application.config.active_support.halt_callback_chains_on_return_false = false +ActiveSupport.halt_callback_chains_on_return_false = false diff --git a/railties/lib/rails/generators/rails/controller/controller_generator.rb b/railties/lib/rails/generators/rails/controller/controller_generator.rb index df615c88b5..6c583e5811 100644 --- a/railties/lib/rails/generators/rails/controller/controller_generator.rb +++ b/railties/lib/rails/generators/rails/controller/controller_generator.rb @@ -13,7 +13,8 @@ module Rails def add_routes unless options[:skip_routes] actions.reverse_each do |action| - route generate_routing_code(action) + # route prepends two spaces onto the front of the string that is passed, this corrects that. + route generate_routing_code(action)[2..-1] end end end @@ -36,12 +37,12 @@ module Rails # namespace :foo do # namespace :bar do namespace_ladder = regular_class_path.each_with_index.map do |ns, i| - indent("namespace :#{ns} do\n", i * 2) + indent(" namespace :#{ns} do\n", i * 2) end.join # Create route # get 'baz/index' - route = indent(%{get '#{file_name}/#{action}'\n}, depth * 2) + route = indent(%{ get '#{file_name}/#{action}'\n}, depth * 2) # Create `end` ladder # end diff --git a/railties/lib/rails/generators/rails/plugin/templates/rails/javascripts.js b/railties/lib/rails/generators/rails/plugin/templates/rails/javascripts.js index c28e5badc6..8913b40f69 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/rails/javascripts.js +++ b/railties/lib/rails/generators/rails/plugin/templates/rails/javascripts.js @@ -7,7 +7,7 @@ // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the // compiled file. // -// Read Sprockets README (https://github.com/sstephenson/sprockets#sprockets-directives) for details +// Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details // about supported directives. // //= require_tree . diff --git a/railties/lib/rails/generators/rails/resource_route/resource_route_generator.rb b/railties/lib/rails/generators/rails/resource_route/resource_route_generator.rb index e4a2bc2b0f..c986f95e67 100644 --- a/railties/lib/rails/generators/rails/resource_route/resource_route_generator.rb +++ b/railties/lib/rails/generators/rails/resource_route/resource_route_generator.rb @@ -29,8 +29,10 @@ module Rails write("end", route_length - index) end - # route prepends two spaces onto the front of the string that is passed, this corrects that - route route_string[2..-1] + # route prepends two spaces onto the front of the string that is passed, this corrects that. + # Also it adds a \n to the end of each line, as route already adds that + # we need to correct that too. + route route_string[2..-2] end private diff --git a/railties/lib/rails/generators/rails/scaffold/templates/scaffold.css b/railties/lib/rails/generators/rails/scaffold/templates/scaffold.css index 69af1e8307..b7818883d1 100644 --- a/railties/lib/rails/generators/rails/scaffold/templates/scaffold.css +++ b/railties/lib/rails/generators/rails/scaffold/templates/scaffold.css @@ -1,8 +1,11 @@ -body { background-color: #fff; color: #333; } +body { + background-color: #fff; + color: #333; +} body, p, ol, ul, td { font-family: verdana, arial, helvetica, sans-serif; - font-size: 13px; + font-size: 13px; line-height: 18px; margin: 33px; } @@ -13,9 +16,18 @@ pre { font-size: 11px; } -a { color: #000; } -a:visited { color: #666; } -a:hover { color: #fff; background-color:#000; } +a { + color: #000; +} + +a:visited { + color: #666; +} + +a:hover { + color: #fff; + background-color: #000; +} th { padding-bottom: 5px; @@ -27,7 +39,8 @@ td { padding-right: 5px; } -div.field, div.actions { +div.field, +div.actions { margin-bottom: 10px; } @@ -56,7 +69,7 @@ div.field, div.actions { padding: 5px 5px 5px 15px; font-size: 12px; margin: -7px; - margin-bottom: 0px; + margin-bottom: 0; background-color: #c00; color: #fff; } diff --git a/railties/lib/rails/generators/test_unit/mailer/templates/preview.rb b/railties/lib/rails/generators/test_unit/mailer/templates/preview.rb index 6b85764a66..b063cbc47b 100644 --- a/railties/lib/rails/generators/test_unit/mailer/templates/preview.rb +++ b/railties/lib/rails/generators/test_unit/mailer/templates/preview.rb @@ -1,9 +1,9 @@ <% module_namespacing do -%> -# Preview all emails at http://localhost:3000/rails/mailers/<%= file_path %> +# Preview all emails at http://localhost:3000/rails/mailers/<%= file_path %>_mailer class <%= class_name %>MailerPreview < ActionMailer::Preview <% actions.each do |action| -%> - # Preview this email at http://localhost:3000/rails/mailers/<%= file_path %>/<%= action %> + # Preview this email at http://localhost:3000/rails/mailers/<%= file_path %>_mailer/<%= action %> def <%= action %> <%= class_name %>Mailer.<%= action %> end diff --git a/railties/lib/rails/generators/test_unit/model/model_generator.rb b/railties/lib/rails/generators/test_unit/model/model_generator.rb index 2826a3ffa1..086588750e 100644 --- a/railties/lib/rails/generators/test_unit/model/model_generator.rb +++ b/railties/lib/rails/generators/test_unit/model/model_generator.rb @@ -19,7 +19,7 @@ module TestUnit # :nodoc: def create_fixture_file if options[:fixture] && options[:fixture_replacement].nil? - template 'fixtures.yml', File.join('test/fixtures', class_path, "#{plural_file_name}.yml") + template 'fixtures.yml', File.join('test/fixtures', class_path, "#{fixture_file_name}.yml") end end diff --git a/railties/lib/rails/ruby_version_check.rb b/railties/lib/rails/ruby_version_check.rb index 9131c51e91..67a19d8a94 100644 --- a/railties/lib/rails/ruby_version_check.rb +++ b/railties/lib/rails/ruby_version_check.rb @@ -1,13 +1,13 @@ -if RUBY_VERSION < '2.2.1' && RUBY_ENGINE == 'ruby' +if RUBY_VERSION < '2.2.2' && RUBY_ENGINE == 'ruby' desc = defined?(RUBY_DESCRIPTION) ? RUBY_DESCRIPTION : "ruby #{RUBY_VERSION} (#{RUBY_RELEASE_DATE})" abort <<-end_message - Rails 5 requires to run on Ruby 2.2.1 or newer. + Rails 5 requires Ruby 2.2.2 or newer. You're running #{desc} - Please upgrade to Ruby 2.2.1 or newer to continue. + Please upgrade to Ruby 2.2.2 or newer to continue. end_message end diff --git a/railties/lib/rails/tasks.rb b/railties/lib/rails/tasks.rb index a5e4d2935e..2c3d278eca 100644 --- a/railties/lib/rails/tasks.rb +++ b/railties/lib/rails/tasks.rb @@ -4,6 +4,7 @@ require 'rake' %w( annotations framework + initializers log middleware misc diff --git a/railties/lib/rails/tasks/initializers.rake b/railties/lib/rails/tasks/initializers.rake new file mode 100644 index 0000000000..2968b5cb53 --- /dev/null +++ b/railties/lib/rails/tasks/initializers.rake @@ -0,0 +1,6 @@ +desc "Print out all defined initializers in the order they are invoked by Rails." +task initializers: :environment do + Rails.application.initializers.tsort_each do |initializer| + puts initializer.name + end +end diff --git a/railties/lib/rails/templates/rails/welcome/index.html.erb b/railties/lib/rails/templates/rails/welcome/index.html.erb index 6726c23fc9..acf04af416 100644 --- a/railties/lib/rails/templates/rails/welcome/index.html.erb +++ b/railties/lib/rails/templates/rails/welcome/index.html.erb @@ -18,7 +18,9 @@ color: #000; } - a {color: #03c} + a { + color: #03c; + } a:hover { background-color: #03c; @@ -64,7 +66,10 @@ height: 64px; } - #header h1, #header h2 {margin: 0} + #header h1, + #header h2 { + margin: 0; + } #header h2 { color: #888; @@ -104,7 +109,9 @@ color: #555; } - #about-content td.value {color: #000} + #about-content td.value { + color: #000; + } #about-content ul { padding: 0; diff --git a/railties/lib/rails/test_help.rb b/railties/lib/rails/test_help.rb index 40a1915b54..5cf44e6331 100644 --- a/railties/lib/rails/test_help.rb +++ b/railties/lib/rails/test_help.rb @@ -2,6 +2,7 @@ # so fixtures aren't loaded into that environment abort("Abort testing: Your Rails environment is running in production mode!") if Rails.env.production? +require "rails/test_unit/minitest_plugin" require 'active_support/testing/autorun' require 'active_support/test_case' require 'action_controller' @@ -9,12 +10,6 @@ require 'action_controller/test_case' require 'action_dispatch/testing/integration' require 'rails/generators/test_case' -# Config Rails backtrace in tests. -require 'rails/backtrace_cleaner' -if ENV["BACKTRACE"].nil? - Minitest.backtrace_filter = Rails.backtrace_cleaner -end - if defined?(ActiveRecord::Base) ActiveRecord::Migration.maintain_test_schema! diff --git a/railties/lib/rails/test_unit/minitest_plugin.rb b/railties/lib/rails/test_unit/minitest_plugin.rb new file mode 100644 index 0000000000..70ce9d3360 --- /dev/null +++ b/railties/lib/rails/test_unit/minitest_plugin.rb @@ -0,0 +1,14 @@ +require "minitest" +require "rails/test_unit/reporter" + +def Minitest.plugin_rails_init(options) + self.reporter << Rails::TestUnitReporter.new(options[:io], options) + if $rails_test_runner && (method = $rails_test_runner.find_method) + options[:filter] = method + end + + if !($rails_test_runner && $rails_test_runner.show_backtrace?) + Minitest.backtrace_filter = Rails.backtrace_cleaner + end +end +Minitest.extensions << 'rails' diff --git a/railties/lib/rails/test_unit/reporter.rb b/railties/lib/rails/test_unit/reporter.rb new file mode 100644 index 0000000000..64e99626eb --- /dev/null +++ b/railties/lib/rails/test_unit/reporter.rb @@ -0,0 +1,22 @@ +require "minitest" + +module Rails + class TestUnitReporter < Minitest::StatisticsReporter + def report + return if results.empty? + io.puts + io.puts "Failed tests:" + io.puts + io.puts aggregated_results + end + + def aggregated_results # :nodoc: + filtered_results = results.dup + filtered_results.reject!(&:skipped?) unless options[:verbose] + filtered_results.map do |result| + location, line = result.method(result.name).source_location + "bin/rails test #{location}:#{line}" + end.join "\n" + end + end +end diff --git a/railties/lib/rails/test_unit/runner.rb b/railties/lib/rails/test_unit/runner.rb new file mode 100644 index 0000000000..5573fa6904 --- /dev/null +++ b/railties/lib/rails/test_unit/runner.rb @@ -0,0 +1,137 @@ +require "optparse" +require "rake/file_list" +require "method_source" + +module Rails + class TestRunner + class Options + def self.parse(args) + options = { backtrace: !ENV["BACKTRACE"].nil?, name: nil, environment: "test" } + + opt_parser = ::OptionParser.new do |opts| + opts.banner = "Usage: bin/rails test [options] [file or directory]" + + opts.separator "" + opts.on("-e", "--environment [ENV]", + "Run tests in the ENV environment") do |env| + options[:environment] = env.strip + end + opts.separator "" + opts.separator "Filter options:" + opts.separator "" + opts.separator <<-DESC + You can run a single test by appending the line number to filename: + + bin/rails test test/models/user_test.rb:27 + + DESC + + opts.on("-n", "--name [NAME]", + "Only run tests matching NAME") do |name| + options[:name] = name + end + opts.on("-p", "--pattern [PATTERN]", + "Only run tests matching PATTERN") do |pattern| + options[:name] = "/#{pattern}/" + end + + opts.separator "" + opts.separator "Output options:" + + opts.on("-b", "--backtrace", + "Show the complete backtrace") do + options[:backtrace] = true + end + + opts.separator "" + opts.separator "Common options:" + + opts.on_tail("-h", "--help", "Show this message") do + puts opts + exit + end + end + + opt_parser.order!(args) + + options[:patterns] = [] + while arg = args.shift + if (file_and_line = arg.split(':')).size > 1 + options[:filename], options[:line] = file_and_line + options[:filename] = File.expand_path options[:filename] + options[:line] &&= options[:line].to_i + else + arg = arg.gsub(':', '') + if Dir.exist?("#{arg}") + options[:patterns] << File.expand_path("#{arg}/**/*_test.rb") + elsif File.file?(arg) + options[:patterns] << File.expand_path(arg) + end + end + end + options + end + end + + def initialize(options = {}) + @options = options + end + + def self.run(arguments) + options = Rails::TestRunner::Options.parse(arguments) + Rails::TestRunner.new(options).run + end + + def run + $rails_test_runner = self + ENV["RAILS_ENV"] = @options[:environment] + run_tests + end + + def find_method + return @options[:name] if @options[:name] + return unless @options[:line] + method = test_methods.find do |location, test_method, start_line, end_line| + location == @options[:filename] && + (start_line..end_line).include?(@options[:line].to_i) + end + method[1] if method + end + + def show_backtrace? + @options[:backtrace] + end + + def test_files + return [@options[:filename]] if @options[:filename] + if @options[:patterns] && @options[:patterns].count > 0 + pattern = @options[:patterns] + else + pattern = "test/**/*_test.rb" + end + Rake::FileList[pattern] + end + + private + def run_tests + test_files.to_a.each do |file| + require File.expand_path file + end + end + + def test_methods + methods_map = [] + suites = Minitest::Runnable.runnables.shuffle + suites.each do |suite_class| + suite_class.runnable_methods.each do |test_method| + method = suite_class.instance_method(test_method) + location = method.source_location + start_line = location.last + end_line = method.source.split("\n").size + start_line - 1 + methods_map << [File.expand_path(location.first), test_method, start_line, end_line] + end + end + methods_map + end + end +end diff --git a/railties/lib/rails/test_unit/testing.rake b/railties/lib/rails/test_unit/testing.rake index d836c0d6d6..0f26621b59 100644 --- a/railties/lib/rails/test_unit/testing.rake +++ b/railties/lib/rails/test_unit/testing.rake @@ -1,11 +1,12 @@ -require 'rake/testtask' -require 'rails/test_unit/sub_test_task' +require "rails/test_unit/runner" task default: :test desc "Runs all tests in test folder" task :test do - Rails::TestTask.test_creator(Rake.application.top_level_tasks).invoke_rake_task + $: << "test" + args = ARGV[0] == "test" ? ARGV[1..-1] : [] + Rails::TestRunner.run(args) end namespace :test do @@ -14,30 +15,30 @@ namespace :test do # If used with Active Record, this task runs before the database schema is synchronized. end - Rails::TestTask.new(:run) do |t| - t.pattern = "test/**/*_test.rb" - end + task :run => %w[test] desc "Run tests quickly, but also reset db" task :db => %w[db:test:prepare test] - Rails::TestTask.new(single: "test:prepare") - ["models", "helpers", "controllers", "mailers", "integration", "jobs"].each do |name| - Rails::TestTask.new(name => "test:prepare") do |t| - t.pattern = "test/#{name}/**/*_test.rb" + task name => "test:prepare" do + $: << "test" + Rails::TestRunner.run(["test/#{name}"]) end end - Rails::TestTask.new(generators: "test:prepare") do |t| - t.pattern = "test/lib/generators/**/*_test.rb" + task :generators => "test:prepare" do + $: << "test" + Rails::TestRunner.run(["test/lib/generators"]) end - Rails::TestTask.new(units: "test:prepare") do |t| - t.pattern = 'test/{models,helpers,unit}/**/*_test.rb' + task :units => "test:prepare" do + $: << "test" + Rails::TestRunner.run(["test/models", "test/helpers", "test/unit"]) end - Rails::TestTask.new(functionals: "test:prepare") do |t| - t.pattern = 'test/{controllers,mailers,functional}/**/*_test.rb' + task :functionals => "test:prepare" do + $: << "test" + Rails::TestRunner.run(["test/controllers", "test/mailers", "test/functional"]) end end diff --git a/railties/railties.gemspec b/railties/railties.gemspec index 7a1c897e3d..afe1603448 100644 --- a/railties/railties.gemspec +++ b/railties/railties.gemspec @@ -7,7 +7,7 @@ Gem::Specification.new do |s| s.summary = 'Tools for creating, working with, and running Rails applications.' s.description = 'Rails internals: application bootup, plugins, generators, and rake tasks.' - s.required_ruby_version = '>= 2.2.0' + s.required_ruby_version = '>= 2.2.2' s.license = 'MIT' @@ -15,10 +15,10 @@ Gem::Specification.new do |s| s.email = 'david@loudthinking.com' s.homepage = 'http://www.rubyonrails.org' - s.files = Dir['CHANGELOG.md', 'README.rdoc', 'RDOC_MAIN.rdoc', 'bin/**/*', 'lib/**/{*,.[a-z]*}'] + s.files = Dir['CHANGELOG.md', 'README.rdoc', 'RDOC_MAIN.rdoc', 'exe/**/*', 'lib/**/{*,.[a-z]*}'] s.require_path = 'lib' - s.bindir = 'bin' + s.bindir = 'exe' s.executables = ['rails'] s.rdoc_options << '--exclude' << '.' @@ -28,6 +28,7 @@ Gem::Specification.new do |s| s.add_dependency 'rake', '>= 0.8.7' s.add_dependency 'thor', '>= 0.18.1', '< 2.0' + s.add_dependency 'method_source' s.add_development_dependency 'actionview', version end diff --git a/railties/test/application/asset_debugging_test.rb b/railties/test/application/asset_debugging_test.rb index 9a571fac3a..acd387256c 100644 --- a/railties/test/application/asset_debugging_test.rb +++ b/railties/test/application/asset_debugging_test.rb @@ -57,8 +57,8 @@ module ApplicationTests class ::PostsController < ActionController::Base ; end get '/posts?debug_assets=true' - assert_match(/<script src="\/assets\/application-([0-z]+)\.js\?body=1"><\/script>/, last_response.body) - assert_match(/<script src="\/assets\/xmlhr-([0-z]+)\.js\?body=1"><\/script>/, last_response.body) + assert_match(/<script src="\/assets\/application(\.self)?-([0-z]+)\.js\?body=1"><\/script>/, last_response.body) + assert_match(/<script src="\/assets\/xmlhr(\.self)?-([0-z]+)\.js\?body=1"><\/script>/, last_response.body) end end end diff --git a/railties/test/application/assets_test.rb b/railties/test/application/assets_test.rb index 0a2f283cce..f6b7d4c855 100644 --- a/railties/test/application/assets_test.rb +++ b/railties/test/application/assets_test.rb @@ -204,7 +204,7 @@ module ApplicationTests app_file "app/assets/javascripts/application.js", "alert();" precompile! - manifest = Dir["#{app_path}/public/assets/manifest-*.json"].first + manifest = Dir["#{app_path}/public/assets/.sprockets-manifest-*.json"].first assets = ActiveSupport::JSON.decode(File.read(manifest)) assert_match(/application-([0-z]+)\.js/, assets["assets"]["application.js"]) @@ -217,7 +217,7 @@ module ApplicationTests precompile! - manifest = Dir["#{app_path}/public/x/manifest-*.json"].first + manifest = Dir["#{app_path}/public/x/.sprockets-manifest-*.json"].first assets = ActiveSupport::JSON.decode(File.read(manifest)) assert_match(/application-([0-z]+)\.js/, assets["assets"]["application.js"]) end @@ -229,7 +229,7 @@ module ApplicationTests ENV["RAILS_ENV"] = "production" precompile! - manifest = Dir["#{app_path}/public/assets/manifest-*.json"].first + manifest = Dir["#{app_path}/public/assets/.sprockets-manifest-*.json"].first assets = ActiveSupport::JSON.decode(File.read(manifest)) asset_path = assets["assets"]["application.js"] @@ -261,7 +261,7 @@ module ApplicationTests ENV["RAILS_ENV"] = "production" precompile! - manifest = Dir["#{app_path}/public/assets/manifest-*.json"].first + manifest = Dir["#{app_path}/public/assets/.sprockets-manifest-*.json"].first assets = ActiveSupport::JSON.decode(File.read(manifest)) asset_path = assets["assets"]["application.css"] @@ -291,7 +291,7 @@ module ApplicationTests precompile! - manifest = Dir["#{app_path}/public/assets/manifest-*.json"].first + manifest = Dir["#{app_path}/public/assets/.sprockets-manifest-*.json"].first assets = ActiveSupport::JSON.decode(File.read(manifest)) assert asset_path = assets["assets"].find { |(k, _)| k && k =~ /.png/ }[1] @@ -437,9 +437,9 @@ module ApplicationTests class ::PostsController < ActionController::Base; end get '/posts', {}, {'HTTPS'=>'off'} - assert_match('src="http://example.com/assets/application.js', last_response.body) + assert_match('src="http://example.com/assets/application.self.js', last_response.body) get '/posts', {}, {'HTTPS'=>'on'} - assert_match('src="https://example.com/assets/application.js', last_response.body) + assert_match('src="https://example.com/assets/application.self.js', last_response.body) end test "asset urls should be protocol-relative if no request is in scope" do diff --git a/railties/test/application/build_original_fullpath_test.rb b/railties/test/application/build_original_fullpath_test.rb deleted file mode 100644 index 647ffb097a..0000000000 --- a/railties/test/application/build_original_fullpath_test.rb +++ /dev/null @@ -1,27 +0,0 @@ -require "abstract_unit" - -module ApplicationTests - class BuildOriginalPathTest < ActiveSupport::TestCase - def test_include_original_PATH_info_in_ORIGINAL_FULLPATH - env = { 'PATH_INFO' => '/foo/' } - assert_equal "/foo/", Rails.application.send(:build_original_fullpath, env) - end - - def test_include_SCRIPT_NAME - env = { - 'SCRIPT_NAME' => '/foo', - 'PATH_INFO' => '/bar' - } - - assert_equal "/foo/bar", Rails.application.send(:build_original_fullpath, env) - end - - def test_include_QUERY_STRING - env = { - 'PATH_INFO' => '/foo', - 'QUERY_STRING' => 'bar', - } - assert_equal "/foo?bar", Rails.application.send(:build_original_fullpath, env) - end - end -end diff --git a/railties/test/application/rake_test.rb b/railties/test/application/rake_test.rb index 2bff21dae5..0648b11813 100644 --- a/railties/test/application/rake_test.rb +++ b/railties/test/application/rake_test.rb @@ -99,7 +99,7 @@ module ApplicationTests end def test_code_statistics_sanity - assert_match "Code LOC: 5 Test LOC: 0 Code to Test Ratio: 1:0.0", + assert_match "Code LOC: 7 Test LOC: 0 Code to Test Ratio: 1:0.0", Dir.chdir(app_path){ `rake stats` } end diff --git a/railties/test/application/test_runner_test.rb b/railties/test/application/test_runner_test.rb index a12f3cfc24..c122b315c0 100644 --- a/railties/test/application/test_runner_test.rb +++ b/railties/test/application/test_runner_test.rb @@ -7,7 +7,6 @@ module ApplicationTests def setup build_app - ENV['RAILS_ENV'] = nil create_schema end @@ -55,7 +54,7 @@ module ApplicationTests create_test_file :models, 'foo' create_test_file :models, 'bar' create_test_file :controllers, 'foobar_controller' - run_test_models_command.tap do |output| + run_test_command("test/models").tap do |output| assert_match "FooTest", output assert_match "BarTest", output assert_match "2 runs, 2 assertions, 0 failures", output @@ -66,7 +65,7 @@ module ApplicationTests create_test_file :helpers, 'foo_helper' create_test_file :helpers, 'bar_helper' create_test_file :controllers, 'foobar_controller' - run_test_helpers_command.tap do |output| + run_test_command("test/helpers").tap do |output| assert_match "FooHelperTest", output assert_match "BarHelperTest", output assert_match "2 runs, 2 assertions, 0 failures", output @@ -74,6 +73,7 @@ module ApplicationTests end def test_run_units + skip "we no longer have the concept of unit tests. Just different directories..." create_test_file :models, 'foo' create_test_file :helpers, 'bar_helper' create_test_file :unit, 'baz_unit' @@ -90,7 +90,7 @@ module ApplicationTests create_test_file :controllers, 'foo_controller' create_test_file :controllers, 'bar_controller' create_test_file :models, 'foo' - run_test_controllers_command.tap do |output| + run_test_command("test/controllers").tap do |output| assert_match "FooControllerTest", output assert_match "BarControllerTest", output assert_match "2 runs, 2 assertions, 0 failures", output @@ -101,7 +101,7 @@ module ApplicationTests create_test_file :mailers, 'foo_mailer' create_test_file :mailers, 'bar_mailer' create_test_file :models, 'foo' - run_test_mailers_command.tap do |output| + run_test_command("test/mailers").tap do |output| assert_match "FooMailerTest", output assert_match "BarMailerTest", output assert_match "2 runs, 2 assertions, 0 failures", output @@ -112,7 +112,7 @@ module ApplicationTests create_test_file :jobs, 'foo_job' create_test_file :jobs, 'bar_job' create_test_file :models, 'foo' - run_test_jobs_command.tap do |output| + run_test_command("test/jobs").tap do |output| assert_match "FooJobTest", output assert_match "BarJobTest", output assert_match "2 runs, 2 assertions, 0 failures", output @@ -120,6 +120,7 @@ module ApplicationTests end def test_run_functionals + skip "we no longer have the concept of functional tests. Just different directories..." create_test_file :mailers, 'foo_mailer' create_test_file :controllers, 'bar_controller' create_test_file :functional, 'baz_functional' @@ -135,7 +136,7 @@ module ApplicationTests def test_run_integration create_test_file :integration, 'foo_integration' create_test_file :models, 'foo' - run_test_integration_command.tap do |output| + run_test_command("test/integration").tap do |output| assert_match "FooIntegration", output assert_match "1 runs, 1 assertions, 0 failures", output end @@ -165,7 +166,7 @@ module ApplicationTests end RUBY - run_test_command('test/unit/chu_2_koi_test.rb test_rikka').tap do |output| + run_test_command('-n test_rikka test/unit/chu_2_koi_test.rb').tap do |output| assert_match "Rikka", output assert_no_match "Sanae", output end @@ -186,7 +187,7 @@ module ApplicationTests end RUBY - run_test_command('test/unit/chu_2_koi_test.rb /rikka/').tap do |output| + run_test_command('-p rikka test/unit/chu_2_koi_test.rb').tap do |output| assert_match "Rikka", output assert_no_match "Sanae", output end @@ -194,18 +195,18 @@ module ApplicationTests def test_load_fixtures_when_running_test_suites create_model_with_fixture - suites = [:models, :helpers, [:units, :unit], :controllers, :mailers, - [:functionals, :functional], :integration] + suites = [:models, :helpers, :controllers, :mailers, :integration] suites.each do |suite, directory| directory ||= suite create_fixture_test directory - assert_match "3 users", run_task(["test:#{suite}"]) + assert_match "3 users", run_test_command("test/#{suite}") Dir.chdir(app_path) { FileUtils.rm_f "test/#{directory}" } end end def test_run_with_model + skip "These feel a bit odd. Not sure we should keep supporting them." create_model_with_fixture create_fixture_test 'models', 'user' assert_match "3 users", run_task(["test models/user"]) @@ -213,6 +214,7 @@ module ApplicationTests end def test_run_different_environment_using_env_var + skip "no longer possible. Running tests in a different environment should be explicit" app_file 'test/unit/env_test.rb', <<-RUBY require 'test_helper' @@ -227,7 +229,7 @@ module ApplicationTests assert_match "development", run_test_command('test/unit/env_test.rb') end - def test_run_different_environment_using_e_tag + def test_run_different_environment env = "development" app_file 'test/unit/env_test.rb', <<-RUBY require 'test_helper' @@ -239,7 +241,7 @@ module ApplicationTests end RUBY - assert_match env, run_test_command("test/unit/env_test.rb RAILS_ENV=#{env}") + assert_match env, run_test_command("-e #{env} test/unit/env_test.rb") end def test_generated_scaffold_works_with_rails_test @@ -248,17 +250,8 @@ module ApplicationTests end private - def run_task(tasks) - Dir.chdir(app_path) { `bundle exec rake #{tasks.join ' '}` } - end - def run_test_command(arguments = 'test/unit/test_test.rb') - run_task ['test', arguments] - end - %w{ mailers models helpers units controllers functionals integration jobs }.each do |type| - define_method("run_test_#{type}_command") do - run_task ["test:#{type}"] - end + Dir.chdir(app_path) { `bin/rails t #{arguments}` } end def create_model_with_fixture diff --git a/railties/test/application/test_test.rb b/railties/test/application/test_test.rb index c7132837b1..61652e5052 100644 --- a/railties/test/application/test_test.rb +++ b/railties/test/application/test_test.rb @@ -65,6 +65,7 @@ module ApplicationTests output = run_test_file('unit/failing_test.rb', env: { "BACKTRACE" => "1" }) assert_match %r{/app/test/unit/failing_test\.rb}, output + assert_match %r{/app/test/unit/failing_test\.rb:4}, output end test "ruby schema migrations" do @@ -300,23 +301,7 @@ Expected: ["id", "name"] end def run_test_file(name, options = {}) - ruby '-Itest', "#{app_path}/test/#{name}", options.deep_merge(env: {"RAILS_ENV" => "test"}) - end - - def ruby(*args) - options = args.extract_options! - env = options.fetch(:env, {}) - env["RUBYLIB"] = $:.join(':') - - Dir.chdir(app_path) do - `#{env_string(env)} #{Gem.ruby} #{args.join(' ')} 2>&1` - end - end - - def env_string(variables) - variables.map do |key, value| - "#{key}='#{value}'" - end.join " " + Dir.chdir(app_path) { `bin/rails test "#{app_path}/test/#{name}" 2>&1` } end end end diff --git a/railties/test/code_statistics_test.rb b/railties/test/code_statistics_test.rb new file mode 100644 index 0000000000..1b1ff80bc1 --- /dev/null +++ b/railties/test/code_statistics_test.rb @@ -0,0 +1,20 @@ +require 'abstract_unit' +require 'rails/code_statistics' + +class CodeStatisticsTest < ActiveSupport::TestCase + def setup + @tmp_path = File.expand_path(File.join(File.dirname(__FILE__), 'fixtures', 'tmp')) + @dir_js = File.expand_path(File.join(File.dirname(__FILE__), 'fixtures', 'tmp', 'lib.js')) + FileUtils.mkdir_p(@dir_js) + end + + def teardown + FileUtils.rm_rf(@tmp_path) + end + + test 'ignores directories that happen to have source files extensions' do + assert_nothing_raised do + @code_statistics = CodeStatistics.new(['tmp dir', @tmp_path]) + end + end +end diff --git a/railties/test/commands/dbconsole_test.rb b/railties/test/commands/dbconsole_test.rb index a3cd1eb0ed..2e8dd99f36 100644 --- a/railties/test/commands/dbconsole_test.rb +++ b/railties/test/commands/dbconsole_test.rb @@ -158,12 +158,6 @@ class Rails::DBConsoleTest < ActiveSupport::TestCase assert_equal 'q1w2e3', ENV['PGPASSWORD'] end - def test_sqlite - start(adapter: 'sqlite', database: 'db') - assert !aborted - assert_equal ['sqlite', 'db'], dbconsole.find_cmd_and_exec_args - end - def test_sqlite3 start(adapter: 'sqlite3', database: 'db.sqlite3') assert !aborted diff --git a/railties/test/generators/actions_test.rb b/railties/test/generators/actions_test.rb index c6de2c1fb9..c0b88089b3 100644 --- a/railties/test/generators/actions_test.rb +++ b/railties/test/generators/actions_test.rb @@ -219,6 +219,30 @@ class ActionsTest < Rails::Generators::TestCase assert_file 'config/routes.rb', /#{Regexp.escape(route_command)}/ end + def test_route_should_add_data_with_an_new_line + run_generator + action :route, "root 'welcome#index'" + route_path = File.expand_path("config/routes.rb", destination_root) + content = File.read(route_path) + + # Remove all of the comments and blank lines from the routes file + content.gsub!(/^ \#.*\n/, '') + content.gsub!(/^\n/, '') + + File.open(route_path, "wb") { |file| file.write(content) } + assert_file "config/routes.rb", /\.routes\.draw do\n root 'welcome#index'\nend\n\z/ + + action :route, "resources :product_lines" + + routes = <<-F +Rails.application.routes.draw do + resources :product_lines + root 'welcome#index' +end +F + assert_file "config/routes.rb", routes + end + def test_readme run_generator Rails::Generators::AppGenerator.expects(:source_root).times(2).returns(destination_root) diff --git a/railties/test/generators/app_generator_test.rb b/railties/test/generators/app_generator_test.rb index 4c5dd70a88..2bfa05a0b8 100644 --- a/railties/test/generators/app_generator_test.rb +++ b/railties/test/generators/app_generator_test.rb @@ -18,6 +18,7 @@ DEFAULT_APP_FILES = %w( app/mailers app/models app/models/concerns + app/jobs app/views/layouts bin/bundle bin/rails @@ -67,6 +68,11 @@ class AppGeneratorTest < Rails::Generators::TestCase assert_file("app/assets/javascripts/application.js") end + def test_application_job_file_present + run_generator + assert_file("app/jobs/application_job.rb") + end + def test_invalid_application_name_raises_an_error content = capture(:stderr){ run_generator [File.join(destination_root, "43-things")] } assert_equal "Invalid application name 43-things. Please give a name which does not start with numbers.\n", content @@ -443,13 +449,6 @@ class AppGeneratorTest < Rails::Generators::TestCase end end - def test_inclusion_of_method_source - run_generator - assert_file "Gemfile" do |content| - assert_gem 'method_source' - end - end - def test_template_from_dir_pwd FileUtils.cd(Rails.root) assert_match(/It works from file!/, run_generator([destination_root, "-m", "lib/template.rb"])) @@ -561,6 +560,14 @@ class AppGeneratorTest < Rails::Generators::TestCase end end + def test_spring_with_dev_option + run_generator [destination_root, "--dev"] + + assert_file "Gemfile" do |content| + assert_no_match(/spring/, content) + end + end + def test_generator_if_skip_turbolinks_is_given run_generator [destination_root, "--skip-turbolinks"] diff --git a/railties/test/generators/controller_generator_test.rb b/railties/test/generators/controller_generator_test.rb index a7d56dd352..1351151afb 100644 --- a/railties/test/generators/controller_generator_test.rb +++ b/railties/test/generators/controller_generator_test.rb @@ -96,6 +96,8 @@ class ControllerGeneratorTest < Rails::Generators::TestCase def test_namespaced_routes_are_created_in_routes run_generator ["admin/dashboard", "index"] - assert_file "config/routes.rb", /namespace :admin do\n\s+get 'dashboard\/index'\n/ + assert_file "config/routes.rb" do |route| + assert_match(/^ namespace :admin do\n get 'dashboard\/index'\n end$/, route) + end end end diff --git a/railties/test/generators/job_generator_test.rb b/railties/test/generators/job_generator_test.rb index a9e0cea94f..7fd8f2062f 100644 --- a/railties/test/generators/job_generator_test.rb +++ b/railties/test/generators/job_generator_test.rb @@ -7,14 +7,14 @@ class JobGeneratorTest < Rails::Generators::TestCase def test_job_skeleton_is_created run_generator ["refresh_counters"] assert_file "app/jobs/refresh_counters_job.rb" do |job| - assert_match(/class RefreshCountersJob < ActiveJob::Base/, job) + assert_match(/class RefreshCountersJob < ApplicationJob/, job) end end def test_job_queue_param run_generator ["refresh_counters", "--queue", "important"] assert_file "app/jobs/refresh_counters_job.rb" do |job| - assert_match(/class RefreshCountersJob < ActiveJob::Base/, job) + assert_match(/class RefreshCountersJob < ApplicationJob/, job) assert_match(/queue_as :important/, job) end end @@ -22,7 +22,7 @@ class JobGeneratorTest < Rails::Generators::TestCase def test_job_namespace run_generator ["admin/refresh_counters", "--queue", "admin"] assert_file "app/jobs/admin/refresh_counters_job.rb" do |job| - assert_match(/class Admin::RefreshCountersJob < ActiveJob::Base/, job) + assert_match(/class Admin::RefreshCountersJob < ApplicationJob/, job) assert_match(/queue_as :admin/, job) end end diff --git a/railties/test/generators/mailer_generator_test.rb b/railties/test/generators/mailer_generator_test.rb index 584e7a82aa..f01e8cd2d9 100644 --- a/railties/test/generators/mailer_generator_test.rb +++ b/railties/test/generators/mailer_generator_test.rb @@ -47,13 +47,13 @@ class MailerGeneratorTest < Rails::Generators::TestCase assert_match(/test "bar"/, test) end assert_file "test/mailers/previews/notifier_mailer_preview.rb" do |preview| - assert_match(/\# Preview all emails at http:\/\/localhost\:3000\/rails\/mailers\/notifier/, preview) + assert_match(/\# Preview all emails at http:\/\/localhost\:3000\/rails\/mailers\/notifier_mailer/, preview) assert_match(/class NotifierMailerPreview < ActionMailer::Preview/, preview) - assert_match(/\# Preview this email at http:\/\/localhost\:3000\/rails\/mailers\/notifier\/foo/, preview) + assert_match(/\# Preview this email at http:\/\/localhost\:3000\/rails\/mailers\/notifier_mailer\/foo/, preview) assert_instance_method :foo, preview do |foo| assert_match(/NotifierMailer.foo/, foo) end - assert_match(/\# Preview this email at http:\/\/localhost\:3000\/rails\/mailers\/notifier\/bar/, preview) + assert_match(/\# Preview this email at http:\/\/localhost\:3000\/rails\/mailers\/notifier_mailer\/bar/, preview) assert_instance_method :bar, preview do |bar| assert_match(/NotifierMailer.bar/, bar) end @@ -129,9 +129,9 @@ class MailerGeneratorTest < Rails::Generators::TestCase assert_match(/en\.farm\.animal_mailer\.moos\.subject/, mailer) end assert_file "test/mailers/previews/farm/animal_mailer_preview.rb" do |preview| - assert_match(/\# Preview all emails at http:\/\/localhost\:3000\/rails\/mailers\/farm\/animal/, preview) + assert_match(/\# Preview all emails at http:\/\/localhost\:3000\/rails\/mailers\/farm\/animal_mailer/, preview) assert_match(/class Farm::AnimalMailerPreview < ActionMailer::Preview/, preview) - assert_match(/\# Preview this email at http:\/\/localhost\:3000\/rails\/mailers\/farm\/animal\/moos/, preview) + assert_match(/\# Preview this email at http:\/\/localhost\:3000\/rails\/mailers\/farm\/animal_mailer\/moos/, preview) end assert_file "app/views/farm/animal_mailer/moos.text.erb" assert_file "app/views/farm/animal_mailer/moos.html.erb" diff --git a/railties/test/generators/model_generator_test.rb b/railties/test/generators/model_generator_test.rb index 17a13fbf1f..abd3ff50a4 100644 --- a/railties/test/generators/model_generator_test.rb +++ b/railties/test/generators/model_generator_test.rb @@ -319,6 +319,16 @@ class ModelGeneratorTest < Rails::Generators::TestCase assert_no_file "test/fixtures/accounts.yml" end + def test_fixture_without_pluralization + original_pluralize_table_name = ActiveRecord::Base.pluralize_table_names + ActiveRecord::Base.pluralize_table_names = false + run_generator + assert_generated_fixture("test/fixtures/account.yml", + {"one"=>{"name"=>"MyString", "age"=>1}, "two"=>{"name"=>"MyString", "age"=>1}}) + ensure + ActiveRecord::Base.pluralize_table_names = original_pluralize_table_name + end + def test_check_class_collision content = capture(:stderr){ run_generator ["object"] } assert_match(/The name 'Object' is either already used in your application or reserved/, content) diff --git a/railties/test/isolation/abstract_unit.rb b/railties/test/isolation/abstract_unit.rb index 63209559d7..4509797da1 100644 --- a/railties/test/isolation/abstract_unit.rb +++ b/railties/test/isolation/abstract_unit.rb @@ -322,7 +322,7 @@ Module.new do environment = File.expand_path('../../../../load_paths', __FILE__) require_environment = "-r #{environment}" - `#{Gem.ruby} #{require_environment} #{RAILS_FRAMEWORK_ROOT}/railties/bin/rails new #{app_template_path} --skip-gemfile --no-rc` + `#{Gem.ruby} #{require_environment} #{RAILS_FRAMEWORK_ROOT}/railties/exe/rails new #{app_template_path} --skip-gemfile --no-rc` File.open("#{app_template_path}/config/boot.rb", 'w') do |f| f.puts "require '#{environment}'" f.puts "require 'rails/all'" diff --git a/railties/test/rails_info_controller_test.rb b/railties/test/rails_info_controller_test.rb index d87b51d852..c51503c2b7 100644 --- a/railties/test/rails_info_controller_test.rb +++ b/railties/test/rails_info_controller_test.rb @@ -56,26 +56,26 @@ class InfoControllerTest < ActionController::TestCase test "info controller returns exact matches" do exact_count = -> { JSON(response.body)['exact'].size } - get :routes, path: 'rails/info/route' + get :routes, params: { path: 'rails/info/route' } assert exact_count.call == 0, 'should not match incomplete routes' - get :routes, path: 'rails/info/routes' + get :routes, params: { path: 'rails/info/routes' } assert exact_count.call == 1, 'should match complete routes' - - get :routes, path: 'rails/info/routes.html' + + get :routes, params: { path: 'rails/info/routes.html' } assert exact_count.call == 1, 'should match complete routes with optional parts' end test "info controller returns fuzzy matches" do fuzzy_count = -> { JSON(response.body)['fuzzy'].size } - get :routes, path: 'rails/info' + get :routes, params: { path: 'rails/info' } assert fuzzy_count.call == 2, 'should match incomplete routes' - get :routes, path: 'rails/info/routes' + get :routes, params: { path: 'rails/info/routes' } assert fuzzy_count.call == 1, 'should match complete routes' - - get :routes, path: 'rails/info/routes.html' + + get :routes, params: { path: 'rails/info/routes.html' } assert fuzzy_count.call == 0, 'should match optional parts of route literally' end end diff --git a/railties/test/railties/engine_test.rb b/railties/test/railties/engine_test.rb index 6185742cc1..79bd7a8241 100644 --- a/railties/test/railties/engine_test.rb +++ b/railties/test/railties/engine_test.rb @@ -1155,10 +1155,10 @@ YAML assert_equal "App's bar partial", last_response.body.strip get("/assets/foo.js") - assert_equal "// Bukkit's foo js\n;", last_response.body.strip + assert_equal "// Bukkit's foo js", last_response.body.strip get("/assets/bar.js") - assert_equal "// App's bar js\n;", last_response.body.strip + assert_equal "// App's bar js", last_response.body.strip # ensure that railties are not added twice railties = Rails.application.send(:ordered_railties).map(&:class) diff --git a/railties/test/railties/generators_test.rb b/railties/test/railties/generators_test.rb index 7348d70c56..423ece277e 100644 --- a/railties/test/railties/generators_test.rb +++ b/railties/test/railties/generators_test.rb @@ -30,7 +30,7 @@ module RailtiesTests if File.exist?("#{environment}.rb") require_environment = "-r #{environment}" end - `#{Gem.ruby} #{require_environment} #{RAILS_FRAMEWORK_ROOT}/railties/bin/rails #{cmd}` + `#{Gem.ruby} #{require_environment} #{RAILS_FRAMEWORK_ROOT}/railties/exe/rails #{cmd}` end def build_engine(is_mountable=false) diff --git a/railties/test/test_unit/reporter_test.rb b/railties/test/test_unit/reporter_test.rb new file mode 100644 index 0000000000..77883612f5 --- /dev/null +++ b/railties/test/test_unit/reporter_test.rb @@ -0,0 +1,74 @@ +require 'abstract_unit' +require 'rails/test_unit/reporter' + +class TestUnitReporterTest < ActiveSupport::TestCase + class ExampleTest < Minitest::Test + def woot; end + end + + setup do + @output = StringIO.new + @reporter = Rails::TestUnitReporter.new @output + end + + test "prints rerun snippet to run a single failed test" do + @reporter.record(failed_test) + @reporter.report + + assert_match %r{^bin/rails test .*test/test_unit/reporter_test.rb:6$}, @output.string + assert_rerun_snippet_count 1 + end + + test "prints rerun snippet for every failed test" do + @reporter.record(failed_test) + @reporter.record(failed_test) + @reporter.record(failed_test) + @reporter.report + + assert_rerun_snippet_count 3 + end + + test "does not print snippet for successful and skipped tests" do + @reporter.record(passing_test) + @reporter.record(skipped_test) + @reporter.report + assert_rerun_snippet_count 0 + end + + test "prints rerun snippet for skipped tests if run in verbose mode" do + verbose = Rails::TestUnitReporter.new @output, verbose: true + verbose.record(skipped_test) + verbose.report + + assert_rerun_snippet_count 1 + end + + private + def assert_rerun_snippet_count(snippet_count) + assert_equal snippet_count, @output.string.scan(%r{^bin/rails test }).size + end + + def failed_test + ft = ExampleTest.new(:woot) + ft.failures << begin + raise Minitest::Assertion, "boo" + rescue Minitest::Assertion => e + e + end + ft + end + + def passing_test + ExampleTest.new(:woot) + end + + def skipped_test + st = ExampleTest.new(:woot) + st.failures << begin + raise Minitest::Skip + rescue Minitest::Assertion => e + e + end + st + end +end diff --git a/railties/test/test_unit/runner_test.rb b/railties/test/test_unit/runner_test.rb new file mode 100644 index 0000000000..9ea8b2c114 --- /dev/null +++ b/railties/test/test_unit/runner_test.rb @@ -0,0 +1,111 @@ +require 'abstract_unit' +require 'env_helpers' +require 'rails/test_unit/runner' + +class TestUnitTestRunnerTest < ActiveSupport::TestCase + include EnvHelpers + + setup do + @options = Rails::TestRunner::Options + end + + test "shows the filtered backtrace by default" do + options = @options.parse([]) + assert_not options[:backtrace] + end + + test "has --backtrace (-b) option to show the full backtrace" do + options = @options.parse(["-b"]) + assert options[:backtrace] + + options = @options.parse(["--backtrace"]) + assert options[:backtrace] + end + + test "show full backtrace using BACKTRACE environment variable" do + switch_env "BACKTRACE", "true" do + options = @options.parse([]) + assert options[:backtrace] + end + end + + test "tests run in the test environment by default" do + options = @options.parse([]) + assert_equal "test", options[:environment] + end + + test "can run in a specific environment" do + options = @options.parse(["-e development"]) + assert_equal "development", options[:environment] + end + + test "parse the filename and line" do + file = "test/test_unit/runner_test.rb" + absolute_file = File.expand_path __FILE__ + options = @options.parse(["#{file}:20"]) + assert_equal absolute_file, options[:filename] + assert_equal 20, options[:line] + + options = @options.parse(["#{file}:"]) + assert_equal [absolute_file], options[:patterns] + assert_nil options[:line] + + options = @options.parse([file]) + assert_equal [absolute_file], options[:patterns] + assert_nil options[:line] + end + + test "find_method on same file" do + options = @options.parse(["#{__FILE__}:#{__LINE__}"]) + runner = Rails::TestRunner.new(options) + assert_equal "test_find_method_on_same_file", runner.find_method + end + + test "find_method on a different file" do + options = @options.parse(["foobar.rb:#{__LINE__}"]) + runner = Rails::TestRunner.new(options) + assert_nil runner.find_method + end + + test "run all tests in a directory" do + options = @options.parse([__dir__]) + + assert_equal ["#{__dir__}/**/*_test.rb"], options[:patterns] + assert_nil options[:filename] + assert_nil options[:line] + end + + test "run multiple folders" do + application_dir = File.expand_path("#{__dir__}/../application") + + options = @options.parse([__dir__, application_dir]) + + assert_equal ["#{__dir__}/**/*_test.rb", "#{application_dir}/**/*_test.rb"], options[:patterns] + assert_nil options[:filename] + assert_nil options[:line] + + runner = Rails::TestRunner.new(options) + assert runner.test_files.size > 0 + end + + test "run multiple files and run one file by line" do + line = __LINE__ + absolute_file = File.expand_path(__FILE__) + options = @options.parse([__dir__, "#{__FILE__}:#{line}"]) + + assert_equal ["#{__dir__}/**/*_test.rb"], options[:patterns] + assert_equal absolute_file, options[:filename] + assert_equal line, options[:line] + + runner = Rails::TestRunner.new(options) + assert_equal [absolute_file], runner.test_files, 'Only returns the file that running by line' + end + + test "running multiple files passing line number" do + line = __LINE__ + options = @options.parse(["foobar.rb:8", "#{__FILE__}:#{line}"]) + + assert_equal File.expand_path(__FILE__), options[:filename], 'Returns the last file' + assert_equal line, options[:line] + end +end |