diff options
264 files changed, 3853 insertions, 1479 deletions
@@ -45,6 +45,7 @@ end # Active Support. gem 'dalli', '>= 2.2.1' +gem 'listen', '~> 3.0.5', require: false # Active Job. group :job do @@ -80,7 +81,7 @@ group :test do end platforms :ruby do - gem 'nokogiri', '>= 1.6.7.rc3' + gem 'nokogiri', '>= 1.6.7' # Needed for compiling the ActionDispatch::Journey parser. gem 'racc', '>=1.4.6', require: false diff --git a/Gemfile.lock b/Gemfile.lock index 4d58065000..d7abc12b21 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -137,7 +137,7 @@ PATH activesupport (= 5.0.0.alpha) arel (= 7.0.0.alpha) activesupport (5.0.0.alpha) - concurrent-ruby (~> 1.0.0.pre5, < 2.0.0) + concurrent-ruby (~> 1.0) i18n (~> 0.7) json (~> 1.7, >= 1.7.7) method_source @@ -176,7 +176,7 @@ GEM builder (3.2.2) bunny (2.2.0) amq-protocol (>= 2.0.0) - byebug (6.0.2) + byebug (8.2.0) celluloid (0.17.2) celluloid-essentials celluloid-extras @@ -201,7 +201,7 @@ GEM coffee-script-source execjs coffee-script-source (1.9.1.1) - concurrent-ruby (1.0.0.pre5) + concurrent-ruby (1.0.0) connection_pool (2.2.0) dalli (2.7.4) dante (0.2.0) @@ -212,6 +212,9 @@ GEM delayed_job (>= 3.0, < 5) erubis (2.7.0) execjs (2.6.0) + ffi (1.9.10) + ffi (1.9.10-x64-mingw32) + ffi (1.9.10-x86-mingw32) hitimes (1.2.3) hitimes (1.2.3-x86-mingw32) i18n (0.7.0) @@ -219,12 +222,15 @@ GEM kindlerb (0.1.1) mustache nokogiri + listen (3.0.5) + rb-fsevent (>= 0.9.3) + rb-inotify (>= 0.9) loofah (2.0.3) nokogiri (>= 1.5.9) metaclass (0.0.4) method_source (0.8.2) mime-types (2.6.2) - mini_portile (0.7.0.rc4) + mini_portile2 (2.0.0) minitest (5.3.3) mocha (0.14.0) metaclass (~> 0.0.1) @@ -233,8 +239,8 @@ GEM mustache (1.0.2) mysql (2.9.1) mysql2 (0.4.1) - nokogiri (1.6.7.rc3) - mini_portile (~> 0.7.0.rc4) + nokogiri (1.6.7) + mini_portile2 (~> 2.0.0.rc2) pg (0.18.3) psych (2.0.15) que (0.11.2) @@ -252,6 +258,9 @@ GEM rails-html-sanitizer (1.0.2) loofah (~> 2.0) rake (10.4.2) + rb-fsevent (0.9.6) + rb-inotify (0.9.5) + ffi (>= 0.5.0) rdoc (4.2.0) redcarpet (3.2.3) redis (3.2.1) @@ -333,12 +342,13 @@ DEPENDENCIES jquery-rails! json kindlerb (= 0.1.1) + listen (~> 3.0.5) mail! minitest (< 5.3.4) mocha (~> 0.14) mysql (>= 2.9.0) mysql2 (>= 0.4.0) - nokogiri (>= 1.6.7.rc3) + nokogiri (>= 1.6.7) pg (>= 0.18.0) psych (~> 2.0) qu-rails! @@ -46,18 +46,18 @@ and may also be used independently outside Rails. 1. Install Rails at the command prompt if you haven't yet: - gem install rails + $ gem install rails 2. At the command prompt, create a new Rails application: - rails new myapp + $ rails new myapp where "myapp" is the application name. 3. Change directory to `myapp` and start the web server: - cd myapp - rails server + $ cd myapp + $ rails server Run with `--help` or `-h` for options. diff --git a/actionmailer/README.rdoc b/actionmailer/README.rdoc index e5c2ed8c77..397ebe4201 100644 --- a/actionmailer/README.rdoc +++ b/actionmailer/README.rdoc @@ -146,7 +146,7 @@ The Base class has the full list of configuration options. Here's an example: The latest version of Action Mailer can be installed with RubyGems: - % gem install actionmailer + $ gem install actionmailer Source code can be downloaded as part of the Rails project on GitHub @@ -173,4 +173,3 @@ Bug reports can be filed for the Ruby on Rails project here: Feature requests should be discussed on the rails-core mailing list here: * https://groups.google.com/forum/?fromgroups#!forum/rubyonrails-core - diff --git a/actionmailer/lib/action_mailer/base.rb b/actionmailer/lib/action_mailer/base.rb index cbd7cec70f..cbbf480da8 100644 --- a/actionmailer/lib/action_mailer/base.rb +++ b/actionmailer/lib/action_mailer/base.rb @@ -464,30 +464,26 @@ module ActionMailer # Either a class, string or symbol can be passed in as the Observer. # If a string or symbol is passed in it will be camelized and constantized. def register_observer(observer) - delivery_observer = case observer - when String, Symbol - observer.to_s.camelize.constantize - else - observer - end - - Mail.register_observer(delivery_observer) + Mail.register_observer(observer_class_for(observer)) end # Register an Interceptor which will be called before mail is sent. # Either a class, string or symbol can be passed in as the Interceptor. # If a string or symbol is passed in it will be camelized and constantized. def register_interceptor(interceptor) - delivery_interceptor = case interceptor - when String, Symbol - interceptor.to_s.camelize.constantize - else - interceptor - end - - Mail.register_interceptor(delivery_interceptor) + Mail.register_interceptor(observer_class_for(interceptor)) end + def observer_class_for(value) # :nodoc: + case value + when String, Symbol + value.to_s.camelize.constantize + else + value + end + end + private :observer_class_for + # Returns the name of current mailer. This method is also being used as a path for a view lookup. # If this is an anonymous mailer, this method will return +anonymous+ instead. def mailer_name @@ -545,10 +541,6 @@ module ActionMailer end end - def respond_to?(method, include_private = false) #:nodoc: - super || action_methods.include?(method.to_s) - end - protected def set_payload_for_mail(payload, mail) #:nodoc: @@ -570,6 +562,12 @@ module ActionMailer super end end + + private + + def respond_to_missing?(method, include_all = false) #:nodoc: + action_methods.include?(method.to_s) + end end attr_internal :message @@ -796,52 +794,40 @@ module ActionMailer # end # def mail(headers = {}, &block) - return @_message if @_mail_was_called && headers.blank? && !block - - m = @_message + return message if @_mail_was_called && headers.blank? && !block # At the beginning, do not consider class default for content_type content_type = headers[:content_type] - # Call all the procs (if any) - default_values = {} - self.class.default.each do |k,v| - default_values[k] = v.is_a?(Proc) ? instance_eval(&v) : v - end - - # Handle defaults - headers = headers.reverse_merge(default_values) - headers[:subject] ||= default_i18n_subject + headers = apply_defaults(headers) # Apply charset at the beginning so all fields are properly quoted - m.charset = charset = headers[:charset] + message.charset = charset = headers[:charset] # Set configure delivery behavior - wrap_delivery_behavior!(headers.delete(:delivery_method), headers.delete(:delivery_method_options)) + wrap_delivery_behavior!(headers[:delivery_method], headers[:delivery_method_options]) - # Assign all headers except parts_order, content_type, body, template_name, and template_path - assignable = headers.except(:parts_order, :content_type, :body, :template_name, :template_path) - assignable.each { |k, v| m[k] = v } + assign_headers_to_message(message, headers) # Render the templates and blocks responses = collect_responses(headers, &block) @_mail_was_called = true - create_parts_from_responses(m, responses) + create_parts_from_responses(message, responses) # Setup content type, reapply charset and handle parts order - m.content_type = set_content_type(m, content_type, headers[:content_type]) - m.charset = charset + message.content_type = set_content_type(message, content_type, headers[:content_type]) + message.charset = charset - if m.multipart? - m.body.set_sort_order(headers[:parts_order]) - m.body.sort_parts! + if message.multipart? + message.body.set_sort_order(headers[:parts_order]) + message.body.sort_parts! end - m + message end - protected + protected # Used by #mail to set the content type of the message. # @@ -879,36 +865,61 @@ module ActionMailer I18n.t(:subject, interpolations.merge(scope: [mailer_scope, action_name], default: action_name.humanize)) end - def collect_responses(headers) #:nodoc: - responses = [] + # Emails do not support relative path links. + def self.supports_path? + false + end + + private + + def apply_defaults(headers) + default_values = self.class.default.map do |key, value| + [ + key, + value.is_a?(Proc) ? instance_eval(&value) : value + ] + end.to_h + headers_with_defaults = headers.reverse_merge(default_values) + headers_with_defaults[:subject] ||= default_i18n_subject + headers_with_defaults + end + + def assign_headers_to_message(message, headers) + assignable = headers.except(:parts_order, :content_type, :body, :template_name, + :template_path, :delivery_method, :delivery_method_options) + assignable.each { |k, v| message[k] = v } + end + + def collect_responses(headers) if block_given? collector = ActionMailer::Collector.new(lookup_context) { render(action_name) } yield(collector) - responses = collector.responses + collector.responses elsif headers[:body] - responses << { + [{ body: headers.delete(:body), content_type: self.class.default[:content_type] || "text/plain" - } + }] else - templates_path = headers.delete(:template_path) || self.class.mailer_name - templates_name = headers.delete(:template_name) || action_name + collect_responses_from_templates(headers) + end + end - each_template(Array(templates_path), templates_name) do |template| - self.formats = template.formats + def collect_responses_from_templates(headers) + templates_path = headers[:template_path] || self.class.mailer_name + templates_name = headers[:template_name] || action_name - responses << { - body: render(template: template), - content_type: template.type.to_s - } - end + each_template(Array(templates_path), templates_name).map do |template| + self.formats = template.formats + { + body: render(template: template), + content_type: template.type.to_s + } end - - responses end - def each_template(paths, name, &block) #:nodoc: + def each_template(paths, name, &block) templates = lookup_context.find_all(name, paths) if templates.empty? raise ActionView::MissingTemplate.new(paths, name, paths, false, 'mailer') @@ -917,7 +928,7 @@ module ActionMailer end end - def create_parts_from_responses(m, responses) #:nodoc: + def create_parts_from_responses(m, responses) if responses.size == 1 && !m.has_attachments? responses[0].each { |k,v| m[k] = v } elsif responses.size > 1 && m.has_attachments? @@ -930,17 +941,12 @@ module ActionMailer end end - def insert_part(container, response, charset) #:nodoc: + def insert_part(container, response, charset) response[:charset] ||= charset part = Mail::Part.new(response) container.add_part(part) end - # Emails do not support relative path links. - def self.supports_path? - false - end - ActiveSupport.run_load_hooks(:action_mailer, self) end end diff --git a/actionmailer/lib/action_mailer/log_subscriber.rb b/actionmailer/lib/action_mailer/log_subscriber.rb index 7e9d916b66..2867bf90fb 100644 --- a/actionmailer/lib/action_mailer/log_subscriber.rb +++ b/actionmailer/lib/action_mailer/log_subscriber.rb @@ -8,7 +8,7 @@ module ActionMailer def deliver(event) info do recipients = Array(event.payload[:to]).join(', ') - "\nSent mail to #{recipients} (#{event.duration.round(1)}ms)" + "Sent mail to #{recipients} (#{event.duration.round(1)}ms)" end debug { event.payload[:mail] } @@ -16,7 +16,7 @@ module ActionMailer # An email was received. def receive(event) - info { "\nReceived mail (#{event.duration.round(1)}ms)" } + info { "Received mail (#{event.duration.round(1)}ms)" } debug { event.payload[:mail] } end @@ -25,7 +25,7 @@ module ActionMailer debug do mailer = event.payload[:mailer] action = event.payload[:action] - "\n#{mailer}##{action}: processed outbound mail in #{event.duration.round(1)}ms" + "#{mailer}##{action}: processed outbound mail in #{event.duration.round(1)}ms" end end diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md index 40f447ada7..608512d846 100644 --- a/actionpack/CHANGELOG.md +++ b/actionpack/CHANGELOG.md @@ -1,3 +1,54 @@ +* `ActionController::TestCase` will be moved to it's own gem in Rails 5.1 + + With the speed improvements made to `ActionDispatch::IntegrationTest` we no + longer need to keep two separate code bases for testing controllers. In + Rails 5.1 `ActionController::TestCase` will be deprecated and moved into a + gem outside of Rails source. + + This is a documentation deprecation so that going forward so new tests will use + `ActionDispatch::IntegrationTest` instead of `ActionController::TestCase`. + + *Eileen M. Uchitelle* + +* Add a `response_format` option to `ActionDispatch::DebugExceptions` + to configure the format of the response when errors occur in + development mode. + + If `response_format` is `:default` the debug info will be rendered + in an HTML page. In the other hand, if the provided value is `:api` + the debug info will be rendered in the original response format. + + *Jorge Bejar* + +* Change the `protect_from_forgery` prepend default to `false` + + Per this comment + https://github.com/rails/rails/pull/18334#issuecomment-69234050 we want + `protect_from_forgery` to default to `prepend: false`. + + `protect_from_forgery` will now be insterted into the callback chain at the + point it is called in your application. This is useful for cases where you + want to `protect_from_forgery` after you perform required authentication + callbacks or other callbacks that are required to run after forgery protection. + + If you want `protect_from_forgery` callbacks to always run first, regardless of + position they are called in your application then you can add `prepend: true` + to your `protect_from_forgery` call. + + Example: + + ```ruby + protect_from_forgery prepend: true + ``` + + *Eileen M. Uchitelle* + +* In url_for, never append a question mark to the URL when the query string + is empty anyway. (It used to do that when called like `url_for(controller: + 'x', action: 'y', q: {})`.) + + *Paul Grayson* + * Catch invalid UTF-8 querystring values and respond with BadRequest Check querystring params for invalid UTF-8 characters, and raise an diff --git a/actionpack/README.rdoc b/actionpack/README.rdoc index 44c980b070..0720c66cb9 100644 --- a/actionpack/README.rdoc +++ b/actionpack/README.rdoc @@ -28,7 +28,7 @@ can be used outside of Rails. The latest version of Action Pack can be installed with RubyGems: - % gem install actionpack + $ gem install actionpack Source code can be downloaded as part of the Rails project on GitHub @@ -55,4 +55,3 @@ Bug reports can be filed for the Ruby on Rails project here: Feature requests should be discussed on the rails-core mailing list here: * https://groups.google.com/forum/?fromgroups#!forum/rubyonrails-core - diff --git a/actionpack/lib/abstract_controller/callbacks.rb b/actionpack/lib/abstract_controller/callbacks.rb index 287550db42..d5317e4717 100644 --- a/actionpack/lib/abstract_controller/callbacks.rb +++ b/actionpack/lib/abstract_controller/callbacks.rb @@ -39,8 +39,8 @@ module AbstractController # except: :index, if: -> { true } # the :except option will be ignored. # # ==== Options - # * <tt>only</tt> - The callback should be run only for this action - # * <tt>except</tt> - The callback should be run for all actions except this action + # * <tt>only</tt> - The callback should be run only for this action. + # * <tt>except</tt> - The callback should be run for all actions except this action. def _normalize_callback_options(options) _normalize_callback_option(options, :only, :if) _normalize_callback_option(options, :except, :unless) @@ -59,7 +59,7 @@ module AbstractController # * <tt>names</tt> - A list of valid names that could be used for # callbacks. Note that skipping uses Ruby equality, so it's # impossible to skip a callback defined using an anonymous proc - # using #skip_action_callback + # using #skip_action_callback. def skip_action_callback(*names) ActiveSupport::Deprecation.warn('`skip_action_callback` is deprecated and will be removed in Rails 5.1. Please use skip_before_action, skip_after_action or skip_around_action instead.') skip_before_action(*names, raise: false) @@ -82,8 +82,8 @@ module AbstractController # * <tt>block</tt> - A proc that should be added to the callbacks. # # ==== Block Parameters - # * <tt>name</tt> - The callback to be added - # * <tt>options</tt> - A hash of options to be used when adding the callback + # * <tt>name</tt> - The callback to be added. + # * <tt>options</tt> - A hash of options to be used when adding the callback. def _insert_callbacks(callbacks, block = nil) options = callbacks.extract_options! _normalize_callback_options(options) diff --git a/actionpack/lib/action_controller/metal/conditional_get.rb b/actionpack/lib/action_controller/metal/conditional_get.rb index 89d589c486..d86a793e4c 100644 --- a/actionpack/lib/action_controller/metal/conditional_get.rb +++ b/actionpack/lib/action_controller/metal/conditional_get.rb @@ -66,7 +66,7 @@ module ActionController # # You can also pass an object that responds to +maximum+, such as a # collection of active records. In this case +last_modified+ will be set by - # calling +maximum(:updated_at)+ on the collection (the timestamp of the + # calling <tt>maximum(:updated_at)</tt> on the collection (the timestamp of the # most recently updated record) and the +etag+ by passing the object itself. # # def index diff --git a/actionpack/lib/action_controller/metal/live.rb b/actionpack/lib/action_controller/metal/live.rb index 27b3eb4e58..e3c540bf5f 100644 --- a/actionpack/lib/action_controller/metal/live.rb +++ b/actionpack/lib/action_controller/metal/live.rb @@ -36,8 +36,7 @@ module ActionController extend ActiveSupport::Concern module ClassMethods - def make_response!(response) - request = response.request + def make_response!(request) if request.get_header("HTTP_VERSION") == "HTTP/1.0" super else @@ -223,12 +222,6 @@ module ActionController jar.write self unless committed? end - def before_sending - super - request.cookie_jar.commit! - headers.freeze - end - def build_buffer(response, body) buf = Live::Buffer.new response body.each { |part| buf.write part } @@ -293,9 +286,5 @@ module ActionController super response.close if response end - - def set_response!(response) - @_response = self.class.make_response! response - 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 64f6f7cf51..26c4550f89 100644 --- a/actionpack/lib/action_controller/metal/request_forgery_protection.rb +++ b/actionpack/lib/action_controller/metal/request_forgery_protection.rb @@ -77,6 +77,10 @@ module ActionController #:nodoc: config_accessor :log_warning_on_csrf_failure self.log_warning_on_csrf_failure = true + # Controls whether the Origin header is checked in addition to the CSRF token. + config_accessor :forgery_protection_origin_check + self.forgery_protection_origin_check = false + helper_method :form_authenticity_token helper_method :protect_against_forgery? end @@ -98,13 +102,13 @@ module ActionController #:nodoc: # # Valid Options: # - # * <tt>:only/:except</tt> - Only apply forgery protection to a subset of actions. Like <tt>only: [ :create, :create_all ]</tt>. + # * <tt>:only/:except</tt> - Only apply forgery protection to a subset of actions. For example <tt>only: [ :create, :create_all ]</tt>. # * <tt>:if/:unless</tt> - Turn off the forgery protection entirely depending on the passed Proc or method reference. - # * <tt>:prepend</tt> - By default, the verification of the authentication token is added to the front of the - # callback chain. If you need to make the verification depend on other callbacks, like authentication methods - # (say cookies vs OAuth), this might not work for you. Pass <tt>prepend: false</tt> to just add the - # verification callback in the position of the protect_from_forgery call. This means any callbacks added - # before are run first. + # * <tt>:prepend</tt> - By default, the verification of the authentication token will be added at the position of the + # protect_from_forgery call in your application. This means any callbacks added before are run first. This is useful + # when you want your forgery protection to depend on other callbacks, like authentication methods (Oauth vs Cookie auth). + # + # If you need to add verification to the beginning of the callback chain, use <tt>prepend: true</tt>. # * <tt>:with</tt> - Set the method to handle unverified request. # # Valid unverified request handling methods are: @@ -112,7 +116,7 @@ module ActionController #:nodoc: # * <tt>:reset_session</tt> - Resets the session. # * <tt>:null_session</tt> - Provides an empty session during request but doesn't reset it completely. Used as default if <tt>:with</tt> option is not specified. def protect_from_forgery(options = {}) - options = options.reverse_merge(prepend: true) + options = options.reverse_merge(prepend: false) self.forgery_protection_strategy = protection_method_class(options[:with] || :null_session) self.request_forgery_protection_token ||= :authenticity_token @@ -257,8 +261,19 @@ module ActionController #:nodoc: # * Does the X-CSRF-Token header match the form_authenticity_token def verified_request? !protect_against_forgery? || request.get? || request.head? || - valid_authenticity_token?(session, form_authenticity_param) || - valid_authenticity_token?(session, request.headers['X-CSRF-Token']) + (valid_request_origin? && any_authenticity_token_valid?) + end + + # Checks if any of the authenticity tokens from the request are valid. + def any_authenticity_token_valid? + request_authenticity_tokens.any? do |token| + valid_authenticity_token?(session, token) + end + end + + # Possible authenticity tokens sent in the request. + def request_authenticity_tokens + [form_authenticity_param, request.x_csrf_token] end # Sets the token value for the current session. @@ -336,5 +351,16 @@ module ActionController #:nodoc: def protect_against_forgery? allow_forgery_protection end + + # Checks if the request originated from the same origin by looking at the + # Origin header. + def valid_request_origin? + if forgery_protection_origin_check + # We accept blank origin headers because some user agents don't send it. + request.origin.nil? || request.origin == request.base_url + else + true + end + end end end diff --git a/actionpack/lib/action_controller/metal/strong_parameters.rb b/actionpack/lib/action_controller/metal/strong_parameters.rb index 130ba61786..8af94551cf 100644 --- a/actionpack/lib/action_controller/metal/strong_parameters.rb +++ b/actionpack/lib/action_controller/metal/strong_parameters.rb @@ -3,6 +3,7 @@ require 'active_support/core_ext/array/wrap' require 'active_support/core_ext/string/filters' require 'active_support/rescuable' require 'action_dispatch/http/upload' +require 'rack/test' require 'stringio' require 'set' diff --git a/actionpack/lib/action_controller/test_case.rb b/actionpack/lib/action_controller/test_case.rb index 442ffd6d7c..c55720859e 100644 --- a/actionpack/lib/action_controller/test_case.rb +++ b/actionpack/lib/action_controller/test_case.rb @@ -7,6 +7,9 @@ require 'action_controller/template_assertions' require 'rails-dom-testing' module ActionController + # :stopdoc: + # ActionController::TestCase will be deprecated and moved to a gem in Rails 5.1. + # Please use ActionDispatch::IntegrationTest going forward. class TestRequest < ActionDispatch::TestRequest #:nodoc: DEFAULT_ENV = ActionDispatch::TestRequest::DEFAULT_ENV.dup DEFAULT_ENV.delete 'PATH_INFO' @@ -658,4 +661,5 @@ module ActionController include Behavior end + # :startdoc: end diff --git a/actionpack/lib/action_dispatch/http/mime_negotiation.rb b/actionpack/lib/action_dispatch/http/mime_negotiation.rb index 7acf91902d..0152c17ed4 100644 --- a/actionpack/lib/action_dispatch/http/mime_negotiation.rb +++ b/actionpack/lib/action_dispatch/http/mime_negotiation.rb @@ -67,6 +67,8 @@ module ActionDispatch v = if params_readable Array(Mime[parameters[:format]]) + elsif format = format_from_path_extension + Array(Mime[format]) elsif use_accept_header && valid_accept_header accepts elsif xhr? @@ -160,6 +162,13 @@ module ActionDispatch def use_accept_header !self.class.ignore_accept_header end + + def format_from_path_extension + path = @env['action_dispatch.original_path'] || @env['PATH_INFO'] + if match = path && path.match(/\.(\w+)\z/) + match.captures.first + end + end end end end diff --git a/actionpack/lib/action_dispatch/http/mime_type.rb b/actionpack/lib/action_dispatch/http/mime_type.rb index b64f660ec5..b8d395854c 100644 --- a/actionpack/lib/action_dispatch/http/mime_type.rb +++ b/actionpack/lib/action_dispatch/http/mime_type.rb @@ -47,15 +47,10 @@ module Mime def const_missing(sym) ext = sym.downcase if Mime[ext] - ActiveSupport::Deprecation.warn <<-eow -Accessing mime types via constants is deprecated. Please change: - - `Mime::#{sym}` - -to: - - `Mime[:#{ext}]` - eow + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Accessing mime types via constants is deprecated. + Please change `Mime::#{sym}` to `Mime[:#{ext}]`. + MSG Mime[ext] else super @@ -65,15 +60,10 @@ to: def const_defined?(sym, inherit = true) ext = sym.downcase if Mime[ext] - ActiveSupport::Deprecation.warn <<-eow -Accessing mime types via constants is deprecated. Please change: - - `Mime.const_defined?(#{sym})` - -to: - - `Mime[:#{ext}]` - eow + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Accessing mime types via constants is deprecated. + Please change `Mime.const_defined?(#{sym})` to `Mime[:#{ext}]`. + MSG true else super diff --git a/actionpack/lib/action_dispatch/http/request.rb b/actionpack/lib/action_dispatch/http/request.rb index ea61ad0c02..29cf821090 100644 --- a/actionpack/lib/action_dispatch/http/request.rb +++ b/actionpack/lib/action_dispatch/http/request.rb @@ -36,8 +36,8 @@ module ActionDispatch HTTP_ACCEPT HTTP_ACCEPT_CHARSET HTTP_ACCEPT_ENCODING HTTP_ACCEPT_LANGUAGE HTTP_CACHE_CONTROL HTTP_FROM HTTP_NEGOTIATE HTTP_PRAGMA HTTP_CLIENT_IP - HTTP_X_FORWARDED_FOR HTTP_VERSION - HTTP_X_REQUEST_ID HTTP_X_FORWARDED_HOST + HTTP_X_FORWARDED_FOR HTTP_ORIGIN HTTP_VERSION + HTTP_X_CSRF_TOKEN HTTP_X_REQUEST_ID HTTP_X_FORWARDED_HOST SERVER_ADDR ].freeze @@ -49,6 +49,10 @@ module ActionDispatch METHOD end + def self.empty + new({}) + end + def initialize(env) super @method = nil @@ -59,6 +63,9 @@ module ActionDispatch @ip = nil end + def commit_cookie_jar! # :nodoc: + end + def check_path_parameters! # If any of the path parameters has an invalid encoding then # raise since it's likely to trigger errors further on. @@ -306,10 +313,16 @@ module ActionDispatch end end - # Returns true if the request's content MIME type is - # +application/x-www-form-urlencoded+ or +multipart/form-data+. + # Determine whether the request body contains form-data by checking + # the request Content-Type for one of the media-types: + # "application/x-www-form-urlencoded" or "multipart/form-data". The + # list of form-data media types can be modified through the + # +FORM_DATA_MEDIA_TYPES+ array. + # + # A request body is not assumed to contain form-data when no + # Content-Type header is provided and the request_method is POST. def form_data? - FORM_DATA_MEDIA_TYPES.include?(content_mime_type.to_s) + FORM_DATA_MEDIA_TYPES.include?(media_type) end def body_stream #:nodoc: diff --git a/actionpack/lib/action_dispatch/http/response.rb b/actionpack/lib/action_dispatch/http/response.rb index f0127aa276..9b11111a67 100644 --- a/actionpack/lib/action_dispatch/http/response.rb +++ b/actionpack/lib/action_dispatch/http/response.rb @@ -412,6 +412,8 @@ module ActionDispatch # :nodoc: end def before_sending + headers.freeze + request.commit_cookie_jar! unless committed? end def build_buffer(response, body) diff --git a/actionpack/lib/action_dispatch/http/url.rb b/actionpack/lib/action_dispatch/http/url.rb index 92b10b6d3b..37f41ae988 100644 --- a/actionpack/lib/action_dispatch/http/url.rb +++ b/actionpack/lib/action_dispatch/http/url.rb @@ -81,7 +81,8 @@ module ActionDispatch def add_params(path, params) params = { params: params } unless params.is_a?(Hash) params.reject! { |_,v| v.to_param.nil? } - path << "?#{params.to_query}" unless params.empty? + query = params.to_query + path << "?#{query}" unless query.empty? end def add_anchor(path, anchor) diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb index 2889acaeb8..3477aa8b29 100644 --- a/actionpack/lib/action_dispatch/middleware/cookies.rb +++ b/actionpack/lib/action_dispatch/middleware/cookies.rb @@ -12,6 +12,12 @@ module ActionDispatch end # :stopdoc: + prepend Module.new { + def commit_cookie_jar! + cookie_jar.commit! + end + } + def have_cookie_jar? has_header? 'action_dispatch.cookies'.freeze end @@ -77,6 +83,12 @@ module ActionDispatch # # It can be read using the signed method `cookies.signed[:name]` # cookies.signed[:user_id] = current_user.id # + # # Sets an encrypted cookie value before sending it to the client which + # # prevent users from reading and tampering with its value. + # # The cookie is signed by your app's `secrets.secret_key_base` value. + # # It can be read using the encrypted method `cookies.encrypted[:name]` + # cookies.encrypted[:discount] = 45 + # # # Sets a "permanent" cookie (which expires in 20 years from now). # cookies.permanent[:login] = "XJ-122" # @@ -89,6 +101,7 @@ module ActionDispatch # cookies.size # => 2 # JSON.parse(cookies[:lat_lon]) # => [47.68, -122.37] # cookies.signed[:login] # => "XJ-122" + # cookies.encrypted[:discount] # => 45 # # Example for deleting: # diff --git a/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb b/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb index 66bb74b9c5..b55c937e0c 100644 --- a/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb +++ b/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb @@ -38,9 +38,10 @@ module ActionDispatch end end - def initialize(app, routes_app = nil) - @app = app - @routes_app = routes_app + def initialize(app, routes_app = nil, response_format = :default) + @app = app + @routes_app = routes_app + @response_format = response_format end def call(env) @@ -66,41 +67,79 @@ module ActionDispatch log_error(request, wrapper) if request.get_header('action_dispatch.show_detailed_exceptions') - traces = wrapper.traces - - trace_to_show = 'Application Trace' - if traces[trace_to_show].empty? && wrapper.rescue_template != 'routing_error' - trace_to_show = 'Full Trace' + case @response_format + when :api + render_for_api_application(request, wrapper) + when :default + render_for_default_application(request, wrapper) end + else + raise exception + end + end - if source_to_show = traces[trace_to_show].first - source_to_show_id = source_to_show[:id] - end + def render_for_default_application(request, wrapper) + template = create_template(request, wrapper) + file = "rescues/#{wrapper.rescue_template}" - template = DebugView.new([RESCUES_TEMPLATE_PATH], - request: request, - exception: wrapper.exception, - traces: traces, - show_source_idx: source_to_show_id, - trace_to_show: trace_to_show, - routes_inspector: routes_inspector(exception), - source_extracts: wrapper.source_extracts, - line_number: wrapper.line_number, - file: wrapper.file - ) - file = "rescues/#{wrapper.rescue_template}" - - if request.xhr? - body = template.render(template: file, layout: false, formats: [:text]) - format = "text/plain" - else - body = template.render(template: file, layout: 'rescues/layout') - format = "text/html" - end - render(wrapper.status_code, body, format) + if request.xhr? + body = template.render(template: file, layout: false, formats: [:text]) + format = "text/plain" else - raise exception + body = template.render(template: file, layout: 'rescues/layout') + format = "text/html" end + render(wrapper.status_code, body, format) + end + + def render_for_api_application(request, wrapper) + body = { + status: wrapper.status_code, + error: Rack::Utils::HTTP_STATUS_CODES.fetch( + wrapper.status_code, + Rack::Utils::HTTP_STATUS_CODES[500] + ), + exception: wrapper.exception.inspect, + traces: wrapper.traces + } + + content_type = request.formats.first + to_format = "to_#{content_type.to_sym}" + + if content_type && body.respond_to?(to_format) + formatted_body = body.public_send(to_format) + format = content_type + else + formatted_body = body.to_json + format = Mime[:json] + end + + render(wrapper.status_code, formatted_body, format) + end + + def create_template(request, wrapper) + traces = wrapper.traces + + trace_to_show = 'Application Trace' + if traces[trace_to_show].empty? && wrapper.rescue_template != 'routing_error' + trace_to_show = 'Full Trace' + end + + if source_to_show = traces[trace_to_show].first + source_to_show_id = source_to_show[:id] + end + + DebugView.new([RESCUES_TEMPLATE_PATH], + request: request, + exception: wrapper.exception, + traces: traces, + show_source_idx: source_to_show_id, + trace_to_show: trace_to_show, + routes_inspector: routes_inspector(wrapper.exception), + source_extracts: wrapper.source_extracts, + line_number: wrapper.line_number, + file: wrapper.file + ) end def render(status, body, format) diff --git a/actionpack/lib/action_dispatch/middleware/remote_ip.rb b/actionpack/lib/action_dispatch/middleware/remote_ip.rb index aee2334da9..31b75498b6 100644 --- a/actionpack/lib/action_dispatch/middleware/remote_ip.rb +++ b/actionpack/lib/action_dispatch/middleware/remote_ip.rb @@ -43,7 +43,7 @@ module ActionDispatch # Create a new +RemoteIp+ middleware instance. # - # The +check_ip_spoofing+ option is on by default. When on, an exception + # The +ip_spoofing_check+ option is on by default. When on, an exception # is raised if it looks like the client is trying to lie about its own IP # address. It makes sense to turn off this check on sites aimed at non-IP # clients (like WAP devices), or behind proxies that set headers in an @@ -57,9 +57,9 @@ module ActionDispatch # with your proxy servers after it. If your proxies aren't removed, pass # them in via the +custom_proxies+ parameter. That way, the middleware will # ignore those IP addresses, and return the one that you want. - def initialize(app, check_ip_spoofing = true, custom_proxies = nil) + def initialize(app, ip_spoofing_check = true, custom_proxies = nil) @app = app - @check_ip = check_ip_spoofing + @check_ip = ip_spoofing_check @proxies = if custom_proxies.blank? TRUSTED_PROXIES elsif custom_proxies.respond_to?(:any?) @@ -116,10 +116,18 @@ module ActionDispatch forwarded_ips = ips_from(@req.x_forwarded_for).reverse # +Client-Ip+ and +X-Forwarded-For+ should not, generally, both be set. - # If they are both set, it means that this request passed through two - # proxies with incompatible IP header conventions, and there is no way - # for us to determine which header is the right one after the fact. - # Since we have no idea, we give up and explode. + # If they are both set, it means that either: + # + # 1) This request passed through two proxies with incompatible IP header + # conventions. + # 2) The client passed one of +Client-Ip+ or +X-Forwarded-For+ + # (whichever the proxy servers weren't using) themselves. + # + # Either way, there is no way for us to determine which header is the + # right one after the fact. Since we have no idea, if we are concerned + # about IP spoofing we need to give up and explode. (If you're not + # concerned about IP spoofing you can turn the +ip_spoofing_check+ + # option off.) should_check_ip = @check_ip && client_ips.last && forwarded_ips.last if should_check_ip && !forwarded_ips.include?(client_ips.last) # We don't know which came from the proxy, and which from the user diff --git a/actionpack/lib/action_dispatch/middleware/stack.rb b/actionpack/lib/action_dispatch/middleware/stack.rb index 90e2ae6802..44fc1ee736 100644 --- a/actionpack/lib/action_dispatch/middleware/stack.rb +++ b/actionpack/lib/action_dispatch/middleware/stack.rb @@ -15,7 +15,11 @@ module ActionDispatch def name; klass.name; end def inspect - klass.to_s + if klass.is_a?(Class) + klass.to_s + else + klass.class.to_s + end end def build(app) diff --git a/actionpack/lib/action_dispatch/middleware/static.rb b/actionpack/lib/action_dispatch/middleware/static.rb index 75f8e05a3f..ea9ab3821d 100644 --- a/actionpack/lib/action_dispatch/middleware/static.rb +++ b/actionpack/lib/action_dispatch/middleware/static.rb @@ -15,7 +15,6 @@ module ActionDispatch class FileHandler def initialize(root, index: 'index', headers: {}) @root = root.chomp('/') - @compiled_root = /^#{Regexp.escape(root)}/ @file_server = ::Rack::File.new(@root, headers) @index = index end diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/_source.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/_source.html.erb index e7b913bbe4..e7b913bbe4 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/rescues/_source.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/_source.html.erb diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/_source.text.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/_source.text.erb new file mode 100644 index 0000000000..23a9c7ba3f --- /dev/null +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/_source.text.erb @@ -0,0 +1,8 @@ +<% @source_extracts.first(3).each do |source_extract| %> +<% if source_extract[:code] %> +Extracted source (around line #<%= source_extract[:line_number] %>): + +<% source_extract[:code].each do |line, source| -%> +<%= line == source_extract[:line_number] ? "*#{line}" : "##{line}" -%> <%= source -%><% end -%> +<% end %> +<% end %> diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb index 7c0404ca62..18cd205bad 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -11,7 +11,7 @@ module ActionDispatch class Mapper URL_OPTIONS = [:protocol, :subdomain, :domain, :host, :port] - class Constraints < Endpoint #:nodoc: + class Constraints < Routing::Endpoint #:nodoc: attr_reader :app, :constraints SERVE = ->(app, req) { app.serve req } @@ -600,17 +600,20 @@ module ActionDispatch def mount(app, options = nil) if options path = options.delete(:at) - else - unless Hash === app - raise ArgumentError, "must be called with mount point" - end - + elsif Hash === app options = app app, path = options.find { |k, _| k.respond_to?(:call) } options.delete(app) if app end - raise "A rack application must be specified" unless path + raise ArgumentError, "A rack application must be specified" unless app.respond_to?(:call) + raise ArgumentError, <<-MSG.strip_heredoc unless path + Must be called with mount point + + mount SomeRackApp, at: "some_route" + or + mount(SomeRackApp => "some_route") + MSG rails_app = rails_app? app options[:as] ||= app_name(app, rails_app) diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb index 5f54ea130b..c4228df925 100644 --- a/actionpack/lib/action_dispatch/routing/route_set.rb +++ b/actionpack/lib/action_dispatch/routing/route_set.rb @@ -371,10 +371,6 @@ module ActionDispatch end def eval_block(block) - if block.arity == 1 - raise "You are using the old router DSL which has been removed in Rails 3.1. " << - "Please check how to update your routes file at: http://www.engineyard.com/blog/2010/the-lowdown-on-routes-in-rails-3/" - end mapper = Mapper.new(self) if default_scope mapper.with_default_scope(default_scope, &block) diff --git a/actionpack/lib/action_dispatch/testing/assertions/response.rb b/actionpack/lib/action_dispatch/testing/assertions/response.rb index eab20b075d..c138660a21 100644 --- a/actionpack/lib/action_dispatch/testing/assertions/response.rb +++ b/actionpack/lib/action_dispatch/testing/assertions/response.rb @@ -27,9 +27,11 @@ module ActionDispatch # # Asserts that the response code was status code 401 (unauthorized) # assert_response 401 def assert_response(type, message = nil) + message ||= generate_response_message(type) + if Symbol === type if [:success, :missing, :redirect, :error].include?(type) - assert_predicate @response, RESPONSE_PREDICATES[type], message + assert @response.send(RESPONSE_PREDICATES[type]), message else code = Rack::Utils::SYMBOL_TO_STATUS_CODE[type] if code.nil? @@ -82,6 +84,17 @@ module ActionDispatch handle._compute_redirect_to_location(@request, fragment) end end + + def generate_response_message(type, code = @response.response_code) + "Expected response to be a <#{type}>, but was a <#{code}>" + .concat location_if_redirected + end + + def location_if_redirected + return '' unless @response.redirection? && @response.location.present? + location = normalize_argument_to_redirection(@response.location) + " redirect to <#{location}>" + end end end end diff --git a/actionpack/lib/action_dispatch/testing/integration.rb b/actionpack/lib/action_dispatch/testing/integration.rb index 790f9ea5d2..711ca10419 100644 --- a/actionpack/lib/action_dispatch/testing/integration.rb +++ b/actionpack/lib/action_dispatch/testing/integration.rb @@ -375,6 +375,7 @@ module ActionDispatch @request = ActionDispatch::Request.new(session.last_request.env) response = _mock_session.last_response @response = ActionDispatch::TestResponse.from_response(response) + @response.request = @request @html_document = nil @url_options = nil diff --git a/actionpack/lib/action_dispatch/testing/test_process.rb b/actionpack/lib/action_dispatch/testing/test_process.rb index c28d701b48..eca0439909 100644 --- a/actionpack/lib/action_dispatch/testing/test_process.rb +++ b/actionpack/lib/action_dispatch/testing/test_process.rb @@ -26,7 +26,7 @@ module ActionDispatch @response.redirect_url end - # Shortcut for <tt>Rack::Test::UploadedFile.new(File.join(ActionController::TestCase.fixture_path, path), type)</tt>: + # Shortcut for <tt>Rack::Test::UploadedFile.new(File.join(ActionDispatch::IntegrationTest.fixture_path, path), type)</tt>: # # post :change_avatar, avatar: fixture_file_upload('files/spongebob.png', 'image/png') # diff --git a/actionpack/test/assertions/response_assertions_test.rb b/actionpack/test/assertions/response_assertions_test.rb index 82c747680d..e76c222824 100644 --- a/actionpack/test/assertions/response_assertions_test.rb +++ b/actionpack/test/assertions/response_assertions_test.rb @@ -6,7 +6,12 @@ module ActionDispatch class ResponseAssertionsTest < ActiveSupport::TestCase include ResponseAssertions - FakeResponse = Struct.new(:response_code) do + FakeResponse = Struct.new(:response_code, :location) do + def initialize(*) + super + self.location ||= "http://test.example.com/posts" + end + [:successful, :not_found, :redirection, :server_error].each do |sym| define_method("#{sym}?") do sym == response_code @@ -58,6 +63,37 @@ module ActionDispatch assert_response :succezz } end + + def test_error_message_shows_404_when_404_asserted_for_success + @response = ActionDispatch::Response.new + @response.status = 404 + + error = assert_raises(Minitest::Assertion) { assert_response :success } + expected = "Expected response to be a <success>, but was a <404>" + assert_match expected, error.message + end + + def test_error_message_shows_302_redirect_when_302_asserted_for_success + @response = ActionDispatch::Response.new + @response.status = 302 + @response.location = 'http://test.host/posts/redirect/1' + + error = assert_raises(Minitest::Assertion) { assert_response :success } + expected = "Expected response to be a <success>, but was a <302>" \ + " redirect to <http://test.host/posts/redirect/1>" + assert_match expected, error.message + end + + def test_error_message_shows_302_redirect_when_302_asserted_for_301 + @response = ActionDispatch::Response.new + @response.status = 302 + @response.location = 'http://test.host/posts/redirect/2' + + error = assert_raises(Minitest::Assertion) { assert_response 301 } + expected = "Expected response to be a <301>, but was a <302>" \ + " redirect to <http://test.host/posts/redirect/2>" + assert_match expected, error.message + end end end end diff --git a/actionpack/test/controller/base_test.rb b/actionpack/test/controller/base_test.rb index fb60dbd993..e3f669dbb5 100644 --- a/actionpack/test/controller/base_test.rb +++ b/actionpack/test/controller/base_test.rb @@ -93,7 +93,7 @@ end class ControllerInstanceTests < ActiveSupport::TestCase def setup @empty = EmptyController.new - @empty.set_request!(ActionDispatch::Request.new({})) + @empty.set_request!(ActionDispatch::Request.empty) @empty.set_response!(EmptyController.make_response!(@empty.request)) @contained = Submodule::ContainedEmptyController.new @empty_controllers = [@empty, @contained] diff --git a/actionpack/test/controller/live_stream_test.rb b/actionpack/test/controller/live_stream_test.rb index 843dafac06..aab2d9545d 100644 --- a/actionpack/test/controller/live_stream_test.rb +++ b/actionpack/test/controller/live_stream_test.rb @@ -442,3 +442,42 @@ module ActionController end end end + +class LiveStreamRouterTest < ActionDispatch::IntegrationTest + class TestController < ActionController::Base + include ActionController::Live + + def index + response.headers['Content-Type'] = 'text/event-stream' + sse = SSE.new(response.stream) + sse.write("{\"name\":\"John\"}") + sse.write({ name: "Ryan" }) + ensure + sse.close + end + end + + def self.call(env) + routes.call(env) + end + + def self.routes + @routes ||= ActionDispatch::Routing::RouteSet.new + end + + routes.draw do + get '/test' => 'live_stream_router_test/test#index' + end + + def app + self.class + end + + test "streaming served through the router" do + get "/test" + + assert_response :ok + assert_match(/data: {\"name\":\"John\"}/, response.body) + assert_match(/data: {\"name\":\"Ryan\"}/, response.body) + end +end diff --git a/actionpack/test/controller/mime/respond_to_test.rb b/actionpack/test/controller/mime/respond_to_test.rb index c025c7fa00..76e2d3ff43 100644 --- a/actionpack/test/controller/mime/respond_to_test.rb +++ b/actionpack/test/controller/mime/respond_to_test.rb @@ -661,10 +661,6 @@ class RespondToControllerTest < ActionController::TestCase end def test_variant_inline_syntax - get :variant_inline_syntax, format: :js - assert_equal "text/javascript", @response.content_type - assert_equal "js", @response.body - get :variant_inline_syntax assert_equal "text/html", @response.content_type assert_equal "none", @response.body @@ -674,6 +670,12 @@ class RespondToControllerTest < ActionController::TestCase assert_equal "phone", @response.body end + def test_variant_inline_syntax_with_format + get :variant_inline_syntax, format: :js + assert_equal "text/javascript", @response.content_type + assert_equal "js", @response.body + end + def test_variant_inline_syntax_without_block get :variant_inline_syntax_without_block, params: { v: :phone } assert_equal "text/html", @response.content_type diff --git a/actionpack/test/controller/new_base/bare_metal_test.rb b/actionpack/test/controller/new_base/bare_metal_test.rb index e61f4d241b..c226fa57ee 100644 --- a/actionpack/test/controller/new_base/bare_metal_test.rb +++ b/actionpack/test/controller/new_base/bare_metal_test.rb @@ -26,7 +26,7 @@ module BareMetalTest test "response_body value is wrapped in an array when the value is a String" do controller = BareController.new - controller.set_request!(ActionDispatch::Request.new({})) + controller.set_request!(ActionDispatch::Request.empty) controller.set_response!(BareController.make_response!(controller.request)) controller.index assert_equal ["Hello world"], controller.response_body diff --git a/actionpack/test/controller/request_forgery_protection_test.rb b/actionpack/test/controller/request_forgery_protection_test.rb index 94ffbe3cd0..87a8ed3dc9 100644 --- a/actionpack/test/controller/request_forgery_protection_test.rb +++ b/actionpack/test/controller/request_forgery_protection_test.rb @@ -304,6 +304,41 @@ module RequestForgeryProtectionTests assert_not_blocked { put :index } end + def test_should_allow_post_with_origin_checking_and_correct_origin + forgery_protection_origin_check do + session[:_csrf_token] = @token + @controller.stub :form_authenticity_token, @token do + assert_not_blocked do + @request.set_header 'HTTP_ORIGIN', 'http://test.host' + post :index, params: { custom_authenticity_token: @token } + end + end + end + end + + def test_should_allow_post_with_origin_checking_and_no_origin + forgery_protection_origin_check do + session[:_csrf_token] = @token + @controller.stub :form_authenticity_token, @token do + assert_not_blocked do + post :index, params: { custom_authenticity_token: @token } + end + end + end + end + + def test_should_block_post_with_origin_checking_and_wrong_origin + forgery_protection_origin_check do + session[:_csrf_token] = @token + @controller.stub :form_authenticity_token, @token do + assert_blocked do + @request.set_header 'HTTP_ORIGIN', 'http://bad.host' + post :index, params: { custom_authenticity_token: @token } + end + end + end + end + def test_should_warn_on_missing_csrf_token old_logger = ActionController::Base.logger logger = ActiveSupport::LogSubscriber::TestHelper::MockLogger.new @@ -405,6 +440,16 @@ module RequestForgeryProtectionTests def assert_cross_origin_not_blocked assert_not_blocked { yield } end + + def forgery_protection_origin_check + old_setting = ActionController::Base.forgery_protection_origin_check + ActionController::Base.forgery_protection_origin_check = true + begin + yield + ensure + ActionController::Base.forgery_protection_origin_check = old_setting + end + end end # OK let's get our test on @@ -495,10 +540,10 @@ class PrependProtectForgeryBaseControllerTest < ActionController::TestCase assert_equal(expected_callback_order, @controller.called_callbacks) end - def test_verify_authenticity_token_is_prepended_by_default + def test_verify_authenticity_token_is_not_prepended_by_default @controller = PrependDefaultController.new get :index - expected_callback_order = ["verify_authenticity_token", "custom_action"] + expected_callback_order = ["custom_action", "verify_authenticity_token"] assert_equal(expected_callback_order, @controller.called_callbacks) end end diff --git a/actionpack/test/controller/routing_test.rb b/actionpack/test/controller/routing_test.rb index 4a2b02a003..a39fede5b9 100644 --- a/actionpack/test/controller/routing_test.rb +++ b/actionpack/test/controller/routing_test.rb @@ -289,12 +289,6 @@ class LegacyRouteSetTests < ActiveSupport::TestCase assert_equal({:id=>"1", :filters=>"foo", :format=>"js"}, params) end - def test_draw_with_block_arity_one_raises - assert_raise(RuntimeError) do - rs.draw { |map| map.match '/:controller(/:action(/:id))' } - end - end - def test_specific_controller_action_failure rs.draw do mount lambda {} => "/foo" diff --git a/actionpack/test/dispatch/cookies_test.rb b/actionpack/test/dispatch/cookies_test.rb index 84c244c72a..dfcef14344 100644 --- a/actionpack/test/dispatch/cookies_test.rb +++ b/actionpack/test/dispatch/cookies_test.rb @@ -7,7 +7,7 @@ class CookieJarTest < ActiveSupport::TestCase attr_reader :request def setup - @request = ActionDispatch::Request.new({}) + @request = ActionDispatch::Request.empty end def test_fetch diff --git a/actionpack/test/dispatch/debug_exceptions_test.rb b/actionpack/test/dispatch/debug_exceptions_test.rb index 89c3e75a50..159bf10545 100644 --- a/actionpack/test/dispatch/debug_exceptions_test.rb +++ b/actionpack/test/dispatch/debug_exceptions_test.rb @@ -75,6 +75,13 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest end end + class BoomerAPI < Boomer + def call(env) + env['action_dispatch.show_detailed_exceptions'] = @detailed + raise "puke!" + end + end + RoutesApp = Struct.new(:routes).new(SharedTestRoutes) ProductionApp = ActionDispatch::DebugExceptions.new(Boomer.new(false), RoutesApp) DevelopmentApp = ActionDispatch::DebugExceptions.new(Boomer.new(true), RoutesApp) @@ -166,6 +173,14 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest assert_equal "text/plain", response.content_type assert_match(/RuntimeError\npuke/, body) + Rails.stub :root, Pathname.new('.') do + get "/", headers: xhr_request_env + + assert_response 500 + assert_match 'Extracted source (around line #', body + assert_select 'pre', { count: 0 }, body + end + get "/not_found", headers: xhr_request_env assert_response 404 assert_no_match(/<body>/, body) @@ -197,6 +212,68 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest assert_match(/ActionController::ParameterMissing/, body) end + test "rescue with json error for API request" do + @app = ActionDispatch::DebugExceptions.new(Boomer.new(true), RoutesApp, :api) + + get "/", headers: { 'action_dispatch.show_exceptions' => true } + assert_response 500 + assert_no_match(/<header>/, body) + assert_no_match(/<body>/, body) + assert_equal "application/json", response.content_type + assert_match(/RuntimeError: puke/, body) + + get "/not_found", headers: { 'action_dispatch.show_exceptions' => true } + assert_response 404 + assert_no_match(/<body>/, body) + assert_equal "application/json", response.content_type + assert_match(/#{AbstractController::ActionNotFound.name}/, body) + + get "/method_not_allowed", headers: { 'action_dispatch.show_exceptions' => true } + assert_response 405 + assert_no_match(/<body>/, body) + assert_equal "application/json", response.content_type + assert_match(/ActionController::MethodNotAllowed/, body) + + get "/unknown_http_method", headers: { 'action_dispatch.show_exceptions' => true } + assert_response 405 + assert_no_match(/<body>/, body) + assert_equal "application/json", response.content_type + assert_match(/ActionController::UnknownHttpMethod/, body) + + get "/bad_request", headers: { 'action_dispatch.show_exceptions' => true } + assert_response 400 + assert_no_match(/<body>/, body) + assert_equal "application/json", response.content_type + assert_match(/ActionController::BadRequest/, body) + + get "/parameter_missing", headers: { 'action_dispatch.show_exceptions' => true } + assert_response 400 + assert_no_match(/<body>/, body) + assert_equal "application/json", response.content_type + assert_match(/ActionController::ParameterMissing/, body) + end + + test "rescue with json on API request returns only allowed formats or json as a fallback" do + @app = ActionDispatch::DebugExceptions.new(Boomer.new(true), RoutesApp, :api) + + get "/index.json", headers: { 'action_dispatch.show_exceptions' => true } + assert_response 500 + assert_equal "application/json", response.content_type + assert_match(/RuntimeError: puke/, body) + + get "/index.html", headers: { 'action_dispatch.show_exceptions' => true } + assert_response 500 + assert_no_match(/<header>/, body) + assert_no_match(/<body>/, body) + assert_equal "application/json", response.content_type + assert_match(/RuntimeError: puke/, body) + + get "/index.xml", headers: { 'action_dispatch.show_exceptions' => true } + assert_response 500 + assert_equal "application/xml", response.content_type + assert_match(/RuntimeError: puke/, body) + end + test "does not show filtered parameters" do @app = DevelopmentApp @@ -364,7 +441,7 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest assert_select 'pre code a:first', %r{test/dispatch/debug_exceptions_test\.rb:\d+:in `call} end - # assert framework trace that that threw the error is first + # assert framework trace that threw the error is first assert_select '#Framework-Trace' do assert_select 'pre code a:first', /method_that_raises/ end diff --git a/actionpack/test/dispatch/live_response_test.rb b/actionpack/test/dispatch/live_response_test.rb index 160cdc1582..e4475f4233 100644 --- a/actionpack/test/dispatch/live_response_test.rb +++ b/actionpack/test/dispatch/live_response_test.rb @@ -6,7 +6,7 @@ module ActionController class ResponseTest < ActiveSupport::TestCase def setup @response = Live::Response.new - @response.request = ActionDispatch::Request.new({}) #yolo + @response.request = ActionDispatch::Request.empty end def test_header_merge diff --git a/actionpack/test/dispatch/mapper_test.rb b/actionpack/test/dispatch/mapper_test.rb index e783df855e..df27e41997 100644 --- a/actionpack/test/dispatch/mapper_test.rb +++ b/actionpack/test/dispatch/mapper_test.rb @@ -158,7 +158,7 @@ module ActionDispatch assert_equal '/*path.:format', fakeset.asts.first.to_s end - def test_raising_helpful_error_on_invalid_arguments + def test_raising_error_when_path_is_not_passed fakeset = FakeSet.new mapper = Mapper.new fakeset app = lambda { |env| [200, {}, [""]] } @@ -166,6 +166,18 @@ module ActionDispatch mapper.mount app end end + + def test_raising_error_when_rack_app_is_not_passed + fakeset = FakeSet.new + mapper = Mapper.new fakeset + assert_raises ArgumentError do + mapper.mount 10, as: "exciting" + end + + assert_raises ArgumentError do + mapper.mount as: "exciting" + end + end end end end diff --git a/actionpack/test/dispatch/request/session_test.rb b/actionpack/test/dispatch/request/session_test.rb index ae0e7e93ed..7dcbcc5c21 100644 --- a/actionpack/test/dispatch/request/session_test.rb +++ b/actionpack/test/dispatch/request/session_test.rb @@ -7,7 +7,7 @@ module ActionDispatch attr_reader :req def setup - @req = ActionDispatch::Request.new({}) + @req = ActionDispatch::Request.empty end def test_create_adds_itself_to_env diff --git a/actionpack/test/dispatch/request_test.rb b/actionpack/test/dispatch/request_test.rb index 22240699d9..7dd9d05e62 100644 --- a/actionpack/test/dispatch/request_test.rb +++ b/actionpack/test/dispatch/request_test.rb @@ -4,9 +4,13 @@ class BaseRequestTest < ActiveSupport::TestCase def setup @env = { :ip_spoofing_check => true, - :tld_length => 1, "rack.input" => "foo" } + @original_tld_length = ActionDispatch::Http::URL.tld_length + end + + def teardown + ActionDispatch::Http::URL.tld_length = @original_tld_length end def url_for(options = {}) @@ -19,9 +23,9 @@ class BaseRequestTest < ActiveSupport::TestCase ip_spoofing_check = env.key?(:ip_spoofing_check) ? env.delete(:ip_spoofing_check) : true @trusted_proxies ||= nil ip_app = ActionDispatch::RemoteIp.new(Proc.new { }, ip_spoofing_check, @trusted_proxies) - tld_length = env.key?(:tld_length) ? env.delete(:tld_length) : 1 + ActionDispatch::Http::URL.tld_length = env.delete(:tld_length) if env.key?(:tld_length) + ip_app.call(env) - ActionDispatch::Http::URL.tld_length = tld_length env = @env.merge(env) ActionDispatch::Request.new(env) @@ -254,15 +258,6 @@ end class RequestDomain < BaseRequestTest test "domains" do - request = stub_request 'HTTP_HOST' => 'www.rubyonrails.org' - assert_equal "rubyonrails.org", request.domain - - request = stub_request 'HTTP_HOST' => "www.rubyonrails.co.uk" - assert_equal "rubyonrails.co.uk", request.domain(2) - - request = stub_request 'HTTP_HOST' => "www.rubyonrails.co.uk", :tld_length => 2 - assert_equal "rubyonrails.co.uk", request.domain - request = stub_request 'HTTP_HOST' => "192.168.1.200" assert_nil request.domain @@ -271,25 +266,18 @@ class RequestDomain < BaseRequestTest request = stub_request 'HTTP_HOST' => "192.168.1.200.com" assert_equal "200.com", request.domain - end - test "subdomains" do - request = stub_request 'HTTP_HOST' => "www.rubyonrails.org" - assert_equal %w( www ), request.subdomains - assert_equal "www", request.subdomain + request = stub_request 'HTTP_HOST' => 'www.rubyonrails.org' + assert_equal "rubyonrails.org", request.domain request = stub_request 'HTTP_HOST' => "www.rubyonrails.co.uk" - assert_equal %w( www ), request.subdomains(2) - assert_equal "www", request.subdomain(2) - - request = stub_request 'HTTP_HOST' => "dev.www.rubyonrails.co.uk" - assert_equal %w( dev www ), request.subdomains(2) - assert_equal "dev.www", request.subdomain(2) + assert_equal "rubyonrails.co.uk", request.domain(2) - request = stub_request 'HTTP_HOST' => "dev.www.rubyonrails.co.uk", :tld_length => 2 - assert_equal %w( dev www ), request.subdomains - assert_equal "dev.www", request.subdomain + request = stub_request 'HTTP_HOST' => "www.rubyonrails.co.uk", :tld_length => 2 + assert_equal "rubyonrails.co.uk", request.domain + end + test "subdomains" do request = stub_request 'HTTP_HOST' => "foobar.foobar.com" assert_equal %w( foobar ), request.subdomains assert_equal "foobar", request.subdomain @@ -309,6 +297,22 @@ class RequestDomain < BaseRequestTest request = stub_request 'HTTP_HOST' => nil assert_equal [], request.subdomains assert_equal "", request.subdomain + + request = stub_request 'HTTP_HOST' => "www.rubyonrails.org" + assert_equal %w( www ), request.subdomains + assert_equal "www", request.subdomain + + request = stub_request 'HTTP_HOST' => "www.rubyonrails.co.uk" + assert_equal %w( www ), request.subdomains(2) + assert_equal "www", request.subdomain(2) + + request = stub_request 'HTTP_HOST' => "dev.www.rubyonrails.co.uk" + assert_equal %w( dev www ), request.subdomains(2) + assert_equal "dev.www", request.subdomain(2) + + request = stub_request 'HTTP_HOST' => "dev.www.rubyonrails.co.uk", :tld_length => 2 + assert_equal %w( dev www ), request.subdomains + assert_equal "dev.www", request.subdomain end end @@ -1212,3 +1216,23 @@ class RequestVariant < BaseRequestTest end end end + +class RequestFormData < BaseRequestTest + test 'media_type is from the FORM_DATA_MEDIA_TYPES array' do + assert stub_request('CONTENT_TYPE' => 'application/x-www-form-urlencoded').form_data? + assert stub_request('CONTENT_TYPE' => 'multipart/form-data').form_data? + end + + test 'media_type is not from the FORM_DATA_MEDIA_TYPES array' do + assert !stub_request('CONTENT_TYPE' => 'application/xml').form_data? + assert !stub_request('CONTENT_TYPE' => 'multipart/related').form_data? + end + + test 'no Content-Type header is provided and the request_method is POST' do + request = stub_request('REQUEST_METHOD' => 'POST') + + assert_equal '', request.media_type + assert_equal 'POST', request.request_method + assert !request.form_data? + end +end diff --git a/actionpack/test/dispatch/response_test.rb b/actionpack/test/dispatch/response_test.rb index 981d820ccf..c37679bc5f 100644 --- a/actionpack/test/dispatch/response_test.rb +++ b/actionpack/test/dispatch/response_test.rb @@ -5,6 +5,7 @@ require 'rack/content_length' class ResponseTest < ActiveSupport::TestCase def setup @response = ActionDispatch::Response.create + @response.request = ActionDispatch::Request.empty end def test_can_wait_until_commit @@ -39,6 +40,7 @@ class ResponseTest < ActiveSupport::TestCase def test_response_body_encoding body = ["hello".encode(Encoding::UTF_8)] response = ActionDispatch::Response.new 200, {}, body + response.request = ActionDispatch::Request.empty assert_equal Encoding::UTF_8, response.body.encoding end @@ -261,6 +263,7 @@ class ResponseTest < ActiveSupport::TestCase test "can be explicitly destructured into status, headers and an enumerable body" do response = ActionDispatch::Response.new(404, { 'Content-Type' => 'text/plain' }, ['Not Found']) + response.request = ActionDispatch::Request.empty status, headers, body = *response assert_equal 404, status @@ -356,6 +359,7 @@ class ResponseIntegrationTest < ActionDispatch::IntegrationTest resp.cache_control[:public] = true resp.etag = '123' resp.body = 'Hello' + resp.request = ActionDispatch::Request.empty }.to_a } @@ -392,6 +396,7 @@ class ResponseIntegrationTest < ActionDispatch::IntegrationTest resp.charset = 'utf-16' resp.content_type = Mime[:xml] resp.body = 'Hello' + resp.request = ActionDispatch::Request.empty }.to_a } diff --git a/actionpack/test/dispatch/show_exceptions_test.rb b/actionpack/test/dispatch/show_exceptions_test.rb index ffdf775836..14894d4b82 100644 --- a/actionpack/test/dispatch/show_exceptions_test.rb +++ b/actionpack/test/dispatch/show_exceptions_test.rb @@ -8,7 +8,7 @@ class ShowExceptionsTest < ActionDispatch::IntegrationTest case req.path when "/not_found" raise AbstractController::ActionNotFound - when "/bad_params" + when "/bad_params", "/bad_params.json" begin raise StandardError.new rescue @@ -120,4 +120,18 @@ class ShowExceptionsTest < ActionDispatch::IntegrationTest assert_response 405 assert_equal "", body end + + test "bad params exception is returned in the correct format" do + @app = ProductionApp + + get "/bad_params", headers: { 'action_dispatch.show_exceptions' => true } + assert_equal "text/html; charset=utf-8", response.headers["Content-Type"] + assert_response 400 + assert_match(/400 error/, body) + + get "/bad_params.json", headers: { 'action_dispatch.show_exceptions' => true } + assert_equal "application/json; charset=utf-8", response.headers["Content-Type"] + assert_response 400 + assert_equal("{\"status\":400,\"error\":\"Bad Request\"}", body) + end end diff --git a/actionpack/test/dispatch/url_generation_test.rb b/actionpack/test/dispatch/url_generation_test.rb index fd4ede4d1b..8c9782bb90 100644 --- a/actionpack/test/dispatch/url_generation_test.rb +++ b/actionpack/test/dispatch/url_generation_test.rb @@ -129,6 +129,13 @@ module TestUrlGeneration ) end + test "generating URLS with empty querystring" do + assert_equal "/bars.json", bars_path( + a: {}, + format: 'json' + ) + end + end end diff --git a/actionview/CHANGELOG.md b/actionview/CHANGELOG.md index e5f5961326..0981d47d7a 100644 --- a/actionview/CHANGELOG.md +++ b/actionview/CHANGELOG.md @@ -1,3 +1,18 @@ +* Respect value of `:object` if `:object` is false when rendering. + + Fixes #22260. + + *Yuichiro Kaneko* + +* Generate `week_field` input values using a 1-based index and not a 0-based index + as per the W3 spec: http://www.w3.org/TR/html-markup/datatypes.html#form.data.week + + *Christoph Geschwind* + +* Allow `host` option in `javascript_include_tag` and `stylesheet_link_tag` helpers + + *Grzegorz Witek* + * Restrict `url_for :back` to valid, non-JavaScript URLs. GH#14444 *Damien Burke* @@ -37,7 +52,7 @@ * Add a break_sequence option to word_wrap so you can specify a custom break. - * Mauricio Gomez * + *Mauricio Gomez* * Add wildcard matching to explicit dependencies. diff --git a/actionview/README.rdoc b/actionview/README.rdoc index 8b1f85f748..7a3e5e31d0 100644 --- a/actionview/README.rdoc +++ b/actionview/README.rdoc @@ -9,7 +9,7 @@ used to inline short Ruby snippets inside HTML), and XML Builder. The latest version of Action View can be installed with RubyGems: - % gem install actionview + $ gem install actionview Source code can be downloaded as part of the Rails project on GitHub @@ -36,4 +36,3 @@ Bug reports can be filed for the Ruby on Rails project here: Feature requests should be discussed on the rails-core mailing list here: * https://groups.google.com/forum/?fromgroups#!forum/rubyonrails-core - diff --git a/actionview/lib/action_view/helpers/asset_tag_helper.rb b/actionview/lib/action_view/helpers/asset_tag_helper.rb index fa46a22500..91e934cd64 100644 --- a/actionview/lib/action_view/helpers/asset_tag_helper.rb +++ b/actionview/lib/action_view/helpers/asset_tag_helper.rb @@ -55,7 +55,7 @@ module ActionView # # => <script src="http://www.example.com/xmlhr.js"></script> def javascript_include_tag(*sources) options = sources.extract_options!.stringify_keys - path_options = options.extract!('protocol', 'extname').symbolize_keys + path_options = options.extract!('protocol', 'extname', 'host').symbolize_keys sources.uniq.map { |source| tag_options = { "src" => path_to_javascript(source, path_options) @@ -91,7 +91,7 @@ module ActionView # # <link href="/css/stylish.css" media="screen" rel="stylesheet" /> def stylesheet_link_tag(*sources) options = sources.extract_options!.stringify_keys - path_options = options.extract!('protocol').symbolize_keys + path_options = options.extract!('protocol', 'host').symbolize_keys sources.uniq.map { |source| tag_options = { @@ -205,6 +205,8 @@ module ActionView # # => <img alt="Icon" height="32" src="/icons/icon.gif" width="32" /> # image_tag("/icons/icon.gif", class: "menu_icon") # # => <img alt="Icon" class="menu_icon" src="/icons/icon.gif" /> + # image_tag("/icons/icon.gif", data: { title: 'Rails Application' }) + # # => <img data-title="Rails Application" src="/icons/icon.gif" /> def image_tag(source, options={}) options = options.symbolize_keys check_for_image_tag_errors(options) diff --git a/actionview/lib/action_view/helpers/tags/week_field.rb b/actionview/lib/action_view/helpers/tags/week_field.rb index 5b3d0494e9..835d1667d7 100644 --- a/actionview/lib/action_view/helpers/tags/week_field.rb +++ b/actionview/lib/action_view/helpers/tags/week_field.rb @@ -5,7 +5,7 @@ module ActionView private def format_date(value) - value.try(:strftime, "%Y-W%W") + value.try(:strftime, "%Y-W%V") end end end diff --git a/actionview/lib/action_view/renderer/partial_renderer.rb b/actionview/lib/action_view/renderer/partial_renderer.rb index a78bf0a6e9..bdbf03191a 100644 --- a/actionview/lib/action_view/renderer/partial_renderer.rb +++ b/actionview/lib/action_view/renderer/partial_renderer.rb @@ -337,7 +337,7 @@ module ActionView layout = find_template(layout.to_s, @template_keys) end - object ||= locals[as] + object = locals[as] if object.nil? # Respect object when object is false locals[as] = object if @has_object content = @template.render(view, locals) do |*name| diff --git a/actionview/lib/action_view/renderer/renderer.rb b/actionview/lib/action_view/renderer/renderer.rb index 1bee35d80d..2a3b89aebf 100644 --- a/actionview/lib/action_view/renderer/renderer.rb +++ b/actionview/lib/action_view/renderer/renderer.rb @@ -15,7 +15,7 @@ module ActionView @lookup_context = lookup_context end - # Main render entry point shared by AV and AC. + # Main render entry point shared by Action View and Action Controller. def render(context, options) if options.key?(:partial) render_partial(context, options) diff --git a/actionview/lib/action_view/testing/resolvers.rb b/actionview/lib/action_view/testing/resolvers.rb index dfb7d463b4..63a60542d4 100644 --- a/actionview/lib/action_view/testing/resolvers.rb +++ b/actionview/lib/action_view/testing/resolvers.rb @@ -46,9 +46,8 @@ module ActionView #:nodoc: class NullResolver < PathResolver def query(path, exts, formats) handler, format, variant = extract_handler_and_format_and_variant(path, formats) - [ActionView::Template.new("Template generated by Null Resolver", path, handler, :virtual_path => path, :format => format, :variant => variant)] + [ActionView::Template.new("Template generated by Null Resolver", path.virtual, handler, :virtual_path => path.virtual, :format => format, :variant => variant)] end end - end diff --git a/actionview/test/abstract_unit.rb b/actionview/test/abstract_unit.rb index 2354e91822..79173f730f 100644 --- a/actionview/test/abstract_unit.rb +++ b/actionview/test/abstract_unit.rb @@ -280,7 +280,6 @@ def jruby_skip(message = '') skip message if defined?(JRUBY_VERSION) end -require 'mocha/setup' # FIXME: stop using mocha class ActiveSupport::TestCase include ActiveSupport::Testing::MethodCallAssertions end diff --git a/actionview/test/fixtures/test/_klass.erb b/actionview/test/fixtures/test/_klass.erb new file mode 100644 index 0000000000..9936f86001 --- /dev/null +++ b/actionview/test/fixtures/test/_klass.erb @@ -0,0 +1 @@ +<%= klass.class.name %>
\ No newline at end of file diff --git a/actionview/test/template/asset_tag_helper_test.rb b/actionview/test/template/asset_tag_helper_test.rb index 496b33b35e..fe40010528 100644 --- a/actionview/test/template/asset_tag_helper_test.rb +++ b/actionview/test/template/asset_tag_helper_test.rb @@ -97,6 +97,7 @@ class AssetTagHelperTest < ActionView::TestCase %(javascript_include_tag("bank")) => %(<script src="/javascripts/bank.js" ></script>), %(javascript_include_tag("bank.js")) => %(<script src="/javascripts/bank.js" ></script>), %(javascript_include_tag("bank", :lang => "vbscript")) => %(<script lang="vbscript" src="/javascripts/bank.js" ></script>), + %(javascript_include_tag("bank", :host => "assets.example.com")) => %(<script src="http://assets.example.com/javascripts/bank.js"></script>), %(javascript_include_tag("http://example.com/all")) => %(<script src="http://example.com/all"></script>), %(javascript_include_tag("http://example.com/all.js")) => %(<script src="http://example.com/all.js"></script>), @@ -141,6 +142,7 @@ class AssetTagHelperTest < ActionView::TestCase %(stylesheet_link_tag("/elsewhere/file")) => %(<link href="/elsewhere/file.css" media="screen" rel="stylesheet" />), %(stylesheet_link_tag("subdir/subdir")) => %(<link href="/stylesheets/subdir/subdir.css" media="screen" rel="stylesheet" />), %(stylesheet_link_tag("bank", :media => "all")) => %(<link href="/stylesheets/bank.css" media="all" rel="stylesheet" />), + %(stylesheet_link_tag("bank", :host => "assets.example.com")) => %(<link href="http://assets.example.com/stylesheets/bank.css" media="screen" rel="stylesheet" />), %(stylesheet_link_tag("http://www.example.com/styles/style")) => %(<link href="http://www.example.com/styles/style" media="screen" rel="stylesheet" />), %(stylesheet_link_tag("http://www.example.com/styles/style.css")) => %(<link href="http://www.example.com/styles/style.css" media="screen" rel="stylesheet" />), @@ -191,7 +193,8 @@ class AssetTagHelperTest < ActionView::TestCase %(image_tag("//www.rubyonrails.com/images/rails.png")) => %(<img alt="Rails" src="//www.rubyonrails.com/images/rails.png" />), %(image_tag("mouse.png", :alt => nil)) => %(<img src="/images/mouse.png" />), %(image_tag("data:image/gif;base64,R0lGODlhAQABAID/AMDAwAAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==", :alt => nil)) => %(<img src="data:image/gif;base64,R0lGODlhAQABAID/AMDAwAAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==" />), - %(image_tag("")) => %(<img src="" />) + %(image_tag("")) => %(<img src="" />), + %(image_tag("gold.png", data: { title: 'Rails Application' })) => %(<img data-title="Rails Application" src="/images/gold.png" alt="Gold" />) } FaviconLinkToTag = { diff --git a/actionview/test/template/date_helper_i18n_test.rb b/actionview/test/template/date_helper_i18n_test.rb index 21fca35185..52aef56a61 100644 --- a/actionview/test/template/date_helper_i18n_test.rb +++ b/actionview/test/template/date_helper_i18n_test.rb @@ -46,8 +46,9 @@ class DateHelperDistanceOfTimeInWordsI18nTests < ActiveSupport::TestCase end def test_time_ago_in_words_passes_locale - I18n.expects(:t).with(:less_than_x_minutes, :scope => :'datetime.distance_in_words', :count => 1, :locale => 'ru') - time_ago_in_words(15.seconds.ago, :locale => 'ru') + assert_called_with(I18n, :t, [:less_than_x_minutes, :scope => :'datetime.distance_in_words', :count => 1, :locale => 'ru']) do + time_ago_in_words(15.seconds.ago, :locale => 'ru') + end end def test_distance_of_time_pluralizations @@ -80,8 +81,9 @@ class DateHelperDistanceOfTimeInWordsI18nTests < ActiveSupport::TestCase options = { locale: 'en', scope: :'datetime.distance_in_words' }.merge!(expected_options) options[:count] = count if count - I18n.expects(:t).with(key, options) - distance_of_time_in_words(@from, to, passed_options.merge(locale: 'en')) + assert_called_with(I18n, :t, [key, options]) do + distance_of_time_in_words(@from, to, passed_options.merge(locale: 'en')) + end end end @@ -89,60 +91,74 @@ class DateHelperSelectTagsI18nTests < ActiveSupport::TestCase include ActionView::Helpers::DateHelper attr_reader :request - def setup - @prompt_defaults = {:year => 'Year', :month => 'Month', :day => 'Day', :hour => 'Hour', :minute => 'Minute', :second => 'Seconds'} - - I18n.stubs(:translate).with(:'date.month_names', :locale => 'en').returns Date::MONTHNAMES - end - # select_month def test_select_month_given_use_month_names_option_does_not_translate_monthnames - I18n.expects(:translate).never - select_month(8, :locale => 'en', :use_month_names => Date::MONTHNAMES) + assert_not_called(I18n, :translate) do + select_month(8, :locale => 'en', :use_month_names => Date::MONTHNAMES) + end end def test_select_month_translates_monthnames - I18n.expects(:translate).with(:'date.month_names', :locale => 'en').returns Date::MONTHNAMES - select_month(8, :locale => 'en') + assert_called_with(I18n, :translate, [:'date.month_names', :locale => 'en'], returns: Date::MONTHNAMES) do + select_month(8, :locale => 'en') + end end def test_select_month_given_use_short_month_option_translates_abbr_monthnames - I18n.expects(:translate).with(:'date.abbr_month_names', :locale => 'en').returns Date::ABBR_MONTHNAMES - select_month(8, :locale => 'en', :use_short_month => true) + assert_called_with(I18n, :translate, [:'date.abbr_month_names', :locale => 'en'], returns: Date::ABBR_MONTHNAMES) do + select_month(8, :locale => 'en', :use_short_month => true) + end end def test_date_or_time_select_translates_prompts - @prompt_defaults.each do |key, prompt| - I18n.expects(:translate).with(('datetime.prompts.' + key.to_s).to_sym, :locale => 'en').returns prompt + prompt_defaults = {:year => 'Year', :month => 'Month', :day => 'Day', :hour => 'Hour', :minute => 'Minute', :second => 'Seconds'} + defaults = {[:'date.order', :locale => 'en', :default => []] => %w(year month day)} + + prompt_defaults.each do |key, prompt| + defaults[[('datetime.prompts.' + key.to_s).to_sym, :locale => 'en']] = prompt + end + + prompts_check = -> (prompt, x) do + @prompt_called ||= 0 + + return_value = defaults[[prompt, x]] + @prompt_called += 1 if return_value.present? + + return_value end - I18n.expects(:translate).with(:'date.order', :locale => 'en', :default => []).returns %w(year month day) - datetime_select('post', 'updated_at', :locale => 'en', :include_seconds => true, :prompt => true) + I18n.stub(:translate, prompts_check) do + datetime_select('post', 'updated_at', :locale => 'en', :include_seconds => true, :prompt => true, :use_month_names => Date::MONTHNAMES) + end + assert_equal defaults.count, @prompt_called end # date_or_time_select def test_date_or_time_select_given_an_order_options_does_not_translate_order - I18n.expects(:translate).never - datetime_select('post', 'updated_at', :order => [:year, :month, :day], :locale => 'en') + assert_not_called(I18n, :translate) do + datetime_select('post', 'updated_at', :order => [:year, :month, :day], :locale => 'en', :use_month_names => Date::MONTHNAMES) + end end def test_date_or_time_select_given_no_order_options_translates_order - I18n.expects(:translate).with(:'date.order', :locale => 'en', :default => []).returns %w(year month day) - datetime_select('post', 'updated_at', :locale => 'en') + assert_called_with(I18n, :translate, [ [:'date.order', :locale => 'en', :default => []], [:"date.month_names", {:locale=>"en"}] ], returns: %w(year month day)) do + datetime_select('post', 'updated_at', :locale => 'en') + end end def test_date_or_time_select_given_invalid_order - I18n.expects(:translate).with(:'date.order', :locale => 'en', :default => []).returns %w(invalid month day) - - assert_raise StandardError do - datetime_select('post', 'updated_at', :locale => 'en') + assert_called_with(I18n, :translate, [:'date.order', :locale => 'en', :default => []], returns: %w(invalid month day)) do + assert_raise StandardError do + datetime_select('post', 'updated_at', :locale => 'en') + end end end def test_date_or_time_select_given_symbol_keys - I18n.expects(:translate).with(:'date.order', :locale => 'en', :default => []).returns [:year, :month, :day] - datetime_select('post', 'updated_at', :locale => 'en') + assert_called_with(I18n, :translate, [ [:'date.order', :locale => 'en', :default => []], [:"date.month_names", {:locale=>"en"}] ], returns: [:year, :month, :day]) do + datetime_select('post', 'updated_at', :locale => 'en') + end end end diff --git a/actionview/test/template/form_helper_test.rb b/actionview/test/template/form_helper_test.rb index e540bf27d9..1f7ff3ca7c 100644 --- a/actionview/test/template/form_helper_test.rb +++ b/actionview/test/template/form_helper_test.rb @@ -1281,7 +1281,7 @@ class FormHelperTest < ActionView::TestCase end def test_week_field - expected = %{<input id="post_written_on" name="post[written_on]" type="week" value="2004-W24" />} + expected = %{<input id="post_written_on" name="post[written_on]" type="week" value="2004-W25" />} assert_dom_equal(expected, week_field("post", "written_on")) end @@ -1292,13 +1292,13 @@ class FormHelperTest < ActionView::TestCase end def test_week_field_with_datetime_value - expected = %{<input id="post_written_on" name="post[written_on]" type="week" value="2004-W24" />} + expected = %{<input id="post_written_on" name="post[written_on]" type="week" value="2004-W25" />} @post.written_on = DateTime.new(2004, 6, 15, 1, 2, 3) assert_dom_equal(expected, week_field("post", "written_on")) end def test_week_field_with_extra_attrs - expected = %{<input id="post_written_on" step="2" max="2010-W51" min="2000-W06" name="post[written_on]" type="week" value="2004-W24" />} + expected = %{<input id="post_written_on" step="2" max="2010-W51" min="2000-W06" name="post[written_on]" type="week" value="2004-W25" />} @post.written_on = DateTime.new(2004, 6, 15, 1, 2, 3) min_value = DateTime.new(2000, 2, 13) max_value = DateTime.new(2010, 12, 23) @@ -1308,13 +1308,19 @@ class FormHelperTest < ActionView::TestCase def test_week_field_with_timewithzone_value previous_time_zone, Time.zone = Time.zone, 'UTC' - expected = %{<input id="post_written_on" name="post[written_on]" type="week" value="2004-W24" />} + expected = %{<input id="post_written_on" name="post[written_on]" type="week" value="2004-W25" />} @post.written_on = Time.zone.parse('2004-06-15 15:30:45') assert_dom_equal(expected, week_field("post", "written_on")) ensure Time.zone = previous_time_zone end + def test_week_field_week_number_base + expected = %{<input id="post_written_on" name="post[written_on]" type="week" value="2015-W01" />} + @post.written_on = DateTime.new(2015, 1, 1, 1, 2, 3) + assert_dom_equal(expected, week_field("post", "written_on")) + end + def test_url_field expected = %{<input id="user_homepage" name="user[homepage]" type="url" />} assert_dom_equal(expected, url_field("user", "homepage")) diff --git a/actionview/test/template/form_options_helper_test.rb b/actionview/test/template/form_options_helper_test.rb index d7daba8bf3..6b97cec34c 100644 --- a/actionview/test/template/form_options_helper_test.rb +++ b/actionview/test/template/form_options_helper_test.rb @@ -17,13 +17,37 @@ class FormOptionsHelperTest < ActionView::TestCase Album = Struct.new('Album', :id, :title, :genre) end - def setup - @fake_timezones = %w(A B C D E).map do |id| - tz = stub(:name => id, :to_s => id) - ActiveSupport::TimeZone.stubs(:[]).with(id).returns(tz) - tz + module FakeZones + FakeZone = Struct.new(:name) do + def to_s; name; end end - ActiveSupport::TimeZone.stubs(:all).returns(@fake_timezones) + + module ClassMethods + def [](id); fake_zones ? fake_zones[id] : super; end + def all; fake_zones ? fake_zones.values : super; end + def dummy; :test; end + end + + def self.prepended(base) + class << base + mattr_accessor(:fake_zones) + prepend ClassMethods + end + end + end + + ActiveSupport::TimeZone.prepend FakeZones + + setup do + ActiveSupport::TimeZone.fake_zones = %w(A B C D E).map do |id| + [ id, FakeZones::FakeZone.new(id) ] + end.to_h + + @fake_timezones = ActiveSupport::TimeZone.all + end + + teardown do + ActiveSupport::TimeZone.fake_zones = nil end def test_collection_options @@ -1163,8 +1187,8 @@ class FormOptionsHelperTest < ActionView::TestCase def test_time_zone_select_with_priority_zones_as_regexp @firm = Firm.new("D") - @fake_timezones.each_with_index do |tz, i| - tz.stubs(:=~).returns(i.zero? || i == 3) + @fake_timezones.each do |tz| + def tz.=~(re); %(A D).include?(name) end end html = time_zone_select("firm", "time_zone", /A|D/) @@ -1179,15 +1203,16 @@ class FormOptionsHelperTest < ActionView::TestCase html end - def test_time_zone_select_with_priority_zones_as_regexp_using_grep_finds_no_zones + def test_time_zone_select_with_priority_zones_is_not_implemented_with_grep @firm = Firm.new("D") - priority_zones = /A|D/ + # `time_zone_select` can't be written with `grep` because Active Support + # time zones don't support implicit string coercion with `to_str`. @fake_timezones.each do |tz| - priority_zones.stubs(:===).with(tz).raises(Exception) + def tz.===(zone); raise Exception; end end - html = time_zone_select("firm", "time_zone", priority_zones) + html = time_zone_select("firm", "time_zone", /A|D/) assert_dom_equal "<select id=\"firm_time_zone\" name=\"firm[time_zone]\">" + "<option value=\"\" disabled=\"disabled\">-------------</option>\n" + "<option value=\"A\">A</option>\n" + diff --git a/actionview/test/template/log_subscriber_test.rb b/actionview/test/template/log_subscriber_test.rb index 7f4c84929f..4776c18b0b 100644 --- a/actionview/test/template/log_subscriber_test.rb +++ b/actionview/test/template/log_subscriber_test.rb @@ -12,13 +12,18 @@ class AVLogSubscriberTest < ActiveSupport::TestCase lookup_context = ActionView::LookupContext.new(view_paths, {}, ["test"]) renderer = ActionView::Renderer.new(lookup_context) @view = ActionView::Base.new(renderer, {}) - Rails.stubs(:root).returns(File.expand_path(FIXTURE_LOAD_PATH)) ActionView::LogSubscriber.attach_to :action_view + unless Rails.respond_to?(:root) + @defined_root = true + def Rails.root; :defined_root; end # Minitest `stub` expects the method to be defined. + end end def teardown super ActiveSupport::LogSubscriber.log_subscribers.clear + # We need to undef `root`, RenderTestCases don't want this to be defined + Rails.instance_eval { undef :root } if @defined_root end def set_logger(logger) @@ -26,66 +31,82 @@ class AVLogSubscriberTest < ActiveSupport::TestCase end def test_render_file_template - @view.render(:file => "test/hello_world") - wait + Rails.stub(:root, File.expand_path(FIXTURE_LOAD_PATH)) do + @view.render(:file => "test/hello_world") + wait - assert_equal 1, @logger.logged(:info).size - assert_match(/Rendered test\/hello_world\.erb/, @logger.logged(:info).last) + assert_equal 1, @logger.logged(:info).size + assert_match(/Rendered test\/hello_world\.erb/, @logger.logged(:info).last) + end end def test_render_text_template - @view.render(:text => "TEXT") - wait + Rails.stub(:root, File.expand_path(FIXTURE_LOAD_PATH)) do + @view.render(:text => "TEXT") + wait - assert_equal 1, @logger.logged(:info).size - assert_match(/Rendered text template/, @logger.logged(:info).last) + assert_equal 1, @logger.logged(:info).size + assert_match(/Rendered text template/, @logger.logged(:info).last) + end end def test_render_inline_template - @view.render(:inline => "<%= 'TEXT' %>") - wait + Rails.stub(:root, File.expand_path(FIXTURE_LOAD_PATH)) do + @view.render(:inline => "<%= 'TEXT' %>") + wait - assert_equal 1, @logger.logged(:info).size - assert_match(/Rendered inline template/, @logger.logged(:info).last) + assert_equal 1, @logger.logged(:info).size + assert_match(/Rendered inline template/, @logger.logged(:info).last) + end end def test_render_partial_template - @view.render(:partial => "test/customer") - wait + Rails.stub(:root, File.expand_path(FIXTURE_LOAD_PATH)) do + @view.render(:partial => "test/customer") + wait - assert_equal 1, @logger.logged(:info).size - assert_match(/Rendered test\/_customer.erb/, @logger.logged(:info).last) + assert_equal 1, @logger.logged(:info).size + assert_match(/Rendered test\/_customer.erb/, @logger.logged(:info).last) + end end def test_render_partial_with_implicit_path - @view.render(Customer.new("david"), :greeting => "hi") - wait + Rails.stub(:root, File.expand_path(FIXTURE_LOAD_PATH)) do + @view.render(Customer.new("david"), :greeting => "hi") + wait - assert_equal 1, @logger.logged(:info).size - assert_match(/Rendered customers\/_customer\.html\.erb/, @logger.logged(:info).last) + assert_equal 1, @logger.logged(:info).size + assert_match(/Rendered customers\/_customer\.html\.erb/, @logger.logged(:info).last) + end end def test_render_collection_template - @view.render(:partial => "test/customer", :collection => [ Customer.new("david"), Customer.new("mary") ]) - wait + Rails.stub(:root, File.expand_path(FIXTURE_LOAD_PATH)) do + @view.render(:partial => "test/customer", :collection => [ Customer.new("david"), Customer.new("mary") ]) + wait - assert_equal 1, @logger.logged(:info).size - assert_match(/Rendered test\/_customer.erb/, @logger.logged(:info).last) + assert_equal 1, @logger.logged(:info).size + assert_match(/Rendered test\/_customer.erb/, @logger.logged(:info).last) + end end def test_render_collection_with_implicit_path - @view.render([ Customer.new("david"), Customer.new("mary") ], :greeting => "hi") - wait + Rails.stub(:root, File.expand_path(FIXTURE_LOAD_PATH)) do + @view.render([ Customer.new("david"), Customer.new("mary") ], :greeting => "hi") + wait - assert_equal 1, @logger.logged(:info).size - assert_match(/Rendered customers\/_customer\.html\.erb/, @logger.logged(:info).last) + assert_equal 1, @logger.logged(:info).size + assert_match(/Rendered customers\/_customer\.html\.erb/, @logger.logged(:info).last) + end end def test_render_collection_template_without_path - @view.render([ GoodCustomer.new("david"), Customer.new("mary") ], :greeting => "hi") - wait + Rails.stub(:root, File.expand_path(FIXTURE_LOAD_PATH)) do + @view.render([ GoodCustomer.new("david"), Customer.new("mary") ], :greeting => "hi") + wait - assert_equal 1, @logger.logged(:info).size - assert_match(/Rendered collection/, @logger.logged(:info).last) + assert_equal 1, @logger.logged(:info).size + assert_match(/Rendered collection/, @logger.logged(:info).last) + end end end diff --git a/actionview/test/template/render_test.rb b/actionview/test/template/render_test.rb index 51bc59edae..84aca222b2 100644 --- a/actionview/test/template/render_test.rb +++ b/actionview/test/template/render_test.rb @@ -247,6 +247,8 @@ module RenderTestCases def test_render_object assert_equal "Hello: david", @view.render(:partial => "test/customer", :object => Customer.new("david")) + assert_equal "FalseClass", @view.render(:partial => "test/klass", :object => false) + assert_equal "NilClass", @view.render(:partial => "test/klass", :object => nil) end def test_render_object_with_array diff --git a/actionview/test/template/test_test.rb b/actionview/test/template/test_test.rb index 88bac85039..e1ff639979 100644 --- a/actionview/test/template/test_test.rb +++ b/actionview/test/template/test_test.rb @@ -41,12 +41,12 @@ class PeopleHelperTest < ActionView::TestCase extend ActiveModel::Naming def to_model; self; end def persisted?; true; end - def self.name; 'Mocha::Mock'; end + def self.name; 'Minitest::Mock'; end }.new "David" the_model = nil extend Module.new { - define_method(:mocha_mock_path) { |model, *args| + define_method(:minitest_mock_path) { |model, *args| the_model = model "/people/1" } diff --git a/actionview/test/template/url_helper_test.rb b/actionview/test/template/url_helper_test.rb index 48d0a9a47a..62fa75bc63 100644 --- a/actionview/test/template/url_helper_test.rb +++ b/actionview/test/template/url_helper_test.rb @@ -38,6 +38,10 @@ class UrlHelperTest < ActiveSupport::TestCase assert_equal "/?a=b&c=d", url_for(hash_for(a: :b, c: :d)) end + def test_url_for_does_not_include_empty_hashes + assert_equal "/", url_for(hash_for(a: {})) + end + def test_url_for_with_back referer = 'http://www.example.com/referer' @controller = Struct.new(:request).new(Struct.new(:env).new("HTTP_REFERER" => referer)) diff --git a/activejob/README.md b/activejob/README.md index f9a3183b1a..7268186c00 100644 --- a/activejob/README.md +++ b/activejob/README.md @@ -44,7 +44,7 @@ end Enqueue a job like so: ```ruby -MyJob.perform_later record # Enqueue a job to be performed as soon the queueing system is free. +MyJob.perform_later record # Enqueue a job to be performed as soon as the queueing system is free. ``` ```ruby @@ -102,7 +102,7 @@ see the API Documentation for [ActiveJob::QueueAdapters](http://api.rubyonrails. The latest version of Active Job can be installed with RubyGems: ``` - % gem install activejob + $ gem install activejob ``` Source code can be downloaded as part of the Rails project on GitHub diff --git a/activejob/lib/active_job/base.rb b/activejob/lib/active_job/base.rb index e5f09f65fb..ff5c69ddc6 100644 --- a/activejob/lib/active_job/base.rb +++ b/activejob/lib/active_job/base.rb @@ -36,7 +36,7 @@ module ActiveJob #:nodoc: # Records that are passed in are serialized/deserialized using Global # ID. More information can be found in Arguments. # - # To enqueue a job to be performed as soon the queueing system is free: + # To enqueue a job to be performed as soon as the queueing system is free: # # ProcessPhotoJob.perform_later(photo) # diff --git a/activejob/test/integration/queuing_test.rb b/activejob/test/integration/queuing_test.rb index e435ed4aa6..d8425c9706 100644 --- a/activejob/test/integration/queuing_test.rb +++ b/activejob/test/integration/queuing_test.rb @@ -78,7 +78,7 @@ class QueuingTest < ActiveSupport::TestCase TestJob.perform_later @id wait_for_jobs_to_finish_for(5.seconds) assert job_executed - assert_equal 'de', job_output + assert_equal 'de', job_executed_in_locale ensure I18n.available_locales = [:en] I18n.locale = :en diff --git a/activejob/test/support/integration/dummy_app_template.rb b/activejob/test/support/integration/dummy_app_template.rb index 0c062a025e..262ca72327 100644 --- a/activejob/test/support/integration/dummy_app_template.rb +++ b/activejob/test/support/integration/dummy_app_template.rb @@ -18,8 +18,11 @@ class TestJob < ActiveJob::Base queue_as :integration_tests def perform(x) - File.open(Rails.root.join("tmp/\#{x}"), "w+") do |f| - f.write I18n.locale + File.open(Rails.root.join("tmp/\#{x}"), "wb+") do |f| + f.write Marshal.dump({ + "locale" => I18n.locale.to_s || "en", + "executed_at" => Time.now.to_r + }) end end end diff --git a/activejob/test/support/integration/test_case_helpers.rb b/activejob/test/support/integration/test_case_helpers.rb index 8319d09520..9897f76fd0 100644 --- a/activejob/test/support/integration/test_case_helpers.rb +++ b/activejob/test/support/integration/test_case_helpers.rb @@ -42,15 +42,23 @@ module TestCaseHelpers end end + def job_file(id) + Dummy::Application.root.join("tmp/#{id}") + end + def job_executed(id=@id) - Dummy::Application.root.join("tmp/#{id}").exist? + job_file(id).exist? + end + + def job_data(id) + Marshal.load(File.binread(job_file(id))) end def job_executed_at(id=@id) - File.new(Dummy::Application.root.join("tmp/#{id}")).ctime + job_data(id)["executed_at"] end - def job_output - File.read Dummy::Application.root.join("tmp/#{@id}") + def job_executed_in_locale(id=@id) + job_data(id)["locale"] end end diff --git a/activemodel/README.rdoc b/activemodel/README.rdoc index 20414c1d61..77f5761993 100644 --- a/activemodel/README.rdoc +++ b/activemodel/README.rdoc @@ -235,7 +235,7 @@ behavior out of the box: The latest version of Active Model can be installed with RubyGems: - % gem install activemodel + $ gem install activemodel Source code can be downloaded as part of the Rails project on GitHub @@ -262,4 +262,3 @@ Bug reports can be filed for the Ruby on Rails project here: Feature requests should be discussed on the rails-core mailing list here: * https://groups.google.com/forum/?fromgroups#!forum/rubyonrails-core - diff --git a/activemodel/lib/active_model/attribute_assignment.rb b/activemodel/lib/active_model/attribute_assignment.rb index 087d11f708..62014cd1cd 100644 --- a/activemodel/lib/active_model/attribute_assignment.rb +++ b/activemodel/lib/active_model/attribute_assignment.rb @@ -27,7 +27,7 @@ module ActiveModel if !new_attributes.respond_to?(:stringify_keys) raise ArgumentError, "When assigning attributes, you must pass a hash as an argument." end - return if new_attributes.blank? + return if new_attributes.nil? || new_attributes.empty? attributes = new_attributes.stringify_keys _assign_attributes(sanitize_for_mass_assignment(attributes)) diff --git a/activemodel/lib/active_model/dirty.rb b/activemodel/lib/active_model/dirty.rb index 0ab8df42f5..6e2e5afd1b 100644 --- a/activemodel/lib/active_model/dirty.rb +++ b/activemodel/lib/active_model/dirty.rb @@ -26,6 +26,10 @@ module ActiveModel # # define_attribute_methods :name # + # def initialize(name) + # @name = name + # end + # # def name # @name # end @@ -54,7 +58,7 @@ module ActiveModel # # A newly instantiated +Person+ object is unchanged: # - # person = Person.new + # person = Person.new("Uncle Bob") # person.changed? # => false # # Change the name: diff --git a/activemodel/lib/active_model/type/time.rb b/activemodel/lib/active_model/type/time.rb index 7101bad566..fe09f63a87 100644 --- a/activemodel/lib/active_model/type/time.rb +++ b/activemodel/lib/active_model/type/time.rb @@ -29,7 +29,11 @@ module ActiveModel return value unless value.is_a?(::String) return if value.empty? - dummy_time_value = "2000-01-01 #{value}" + if value =~ /^2000-01-01/ + dummy_time_value = value + else + dummy_time_value = "2000-01-01 #{value}" + end fast_string_to_time(dummy_time_value) || begin time_hash = ::Date._parse(dummy_time_value) diff --git a/activemodel/test/cases/attribute_assignment_test.rb b/activemodel/test/cases/attribute_assignment_test.rb index 3336691841..287bea719c 100644 --- a/activemodel/test/cases/attribute_assignment_test.rb +++ b/activemodel/test/cases/attribute_assignment_test.rb @@ -1,4 +1,5 @@ require "cases/helper" +require "active_support/core_ext/hash/indifferent_access" require "active_support/hash_with_indifferent_access" class AttributeAssignmentTest < ActiveModel::TestCase @@ -23,13 +24,32 @@ class AttributeAssignmentTest < ActiveModel::TestCase class ErrorFromAttributeWriter < StandardError end - class ProtectedParams < ActiveSupport::HashWithIndifferentAccess + class ProtectedParams + attr_accessor :permitted + alias :permitted? :permitted + + delegate :keys, :key?, :has_key?, :empty?, to: :@parameters + + def initialize(attributes) + @parameters = attributes.with_indifferent_access + @permitted = false + end + def permit! @permitted = true + self + end + + def [](key) + @parameters[key] + end + + def to_h + @parameters end - def permitted? - @permitted ||= false + def stringify_keys + dup end def dup diff --git a/activemodel/test/cases/forbidden_attributes_protection_test.rb b/activemodel/test/cases/forbidden_attributes_protection_test.rb index 3cb204a2c5..d8d757f52a 100644 --- a/activemodel/test/cases/forbidden_attributes_protection_test.rb +++ b/activemodel/test/cases/forbidden_attributes_protection_test.rb @@ -2,12 +2,14 @@ require 'cases/helper' require 'active_support/core_ext/hash/indifferent_access' require 'models/account' -class ProtectedParams < ActiveSupport::HashWithIndifferentAccess +class ProtectedParams attr_accessor :permitted alias :permitted? :permitted + delegate :keys, :key?, :has_key?, :empty?, to: :@parameters + def initialize(attributes) - super(attributes) + @parameters = attributes @permitted = false end @@ -15,6 +17,10 @@ class ProtectedParams < ActiveSupport::HashWithIndifferentAccess @permitted = true self end + + def to_h + @parameters + end end class ActiveModelMassUpdateProtectionTest < ActiveSupport::TestCase diff --git a/activemodel/test/cases/translation_test.rb b/activemodel/test/cases/translation_test.rb index cedc812ec7..2c89388f14 100644 --- a/activemodel/test/cases/translation_test.rb +++ b/activemodel/test/cases/translation_test.rb @@ -88,6 +88,11 @@ class ActiveModelI18nTests < ActiveModel::TestCase assert_equal 'child model', Child.model_name.human end + def test_translated_model_with_namespace + I18n.backend.store_translations 'en', activemodel: { models: { 'person/gender': 'gender model' } } + assert_equal 'gender model', Person::Gender.model_name.human + end + def test_translated_model_names_with_ancestors_fallback I18n.backend.store_translations 'en', activemodel: { models: { person: 'person model' } } assert_equal 'person model', Child.model_name.human diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index c0e3840b40..56a3232ee9 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,72 @@ +* Introduce after_{create,update,delete}_commit callbacks. + + Before: + + after_commit :add_to_index_later, on: :create + after_commit :update_in_index_later, on: :update + after_commit :remove_from_index_later, on: :destroy + + After: + + after_create_commit :add_to_index_later + after_update_commit :update_in_index_later + after_destroy_commit :remove_from_index_later + + Fixes #22515. + + *Genadi Samokovarov* + +* Respect the column default values for `inheritance_column` when + instantiating records through the base class. + + Fixes #17121. + + Example: + + # The schema of BaseModel has `t.string :type, default: 'SubType'` + subtype = BaseModel.new + assert_equals SubType, subtype.class + + *Kuldeep Aggarwal* + +* Fix `rake db:structure:dump` on Postgres when multiple schemas are used. + + Fixes #22346. + + *Nick Muerdter*, *ckoenig* + +* Add schema dumping support for PostgreSQL geometric data types. + + *Ryuta Kamizono* + +* Except keys of `build_record`'s argument from `create_scope` in `initialize_attributes`. + + Fixes #21893. + + *Yuichiro Kaneko* + +* Deprecate `connection.tables` on the SQLite3 and MySQL adapters. + Also deprecate passing arguments to `#tables`. + And deprecate `table_exists?`. + + The `#tables` method of some adapters (mysql, mysql2, sqlite3) would return + both tables and views while others (postgresql) just return tables. To make + their behavior consistent, `#tables` will return only tables in the future. + + The `#table_exists?` method would check both tables and views. To make + their behavior consistent with `#tables`, `#table_exists?` will check only + tables in the future. + + *Yuichiro Kaneko* + +* Improve support for non Active Record objects on `validates_associated` + + Skipping `marked_for_destruction?` when the associated object does not responds + to it make easier to validate virtual associations built on top of Active Model + objects and/or serialized objects that implement a `valid?` instance method. + + *Kassio Borges*, *Lucas Mazza* + * Change connection management middleware to return a new response with a body proxy, rather than mutating the original. @@ -179,9 +248,9 @@ Example: - config.generators do |g| - g.orm :active_record, primary_key_type: :uuid - end + config.generators do |g| + g.orm :active_record, primary_key_type: :uuid + end *Jon McCartie* @@ -257,10 +326,10 @@ To load the fixtures file `accounts.yml` as the `User` model, use: - _fixture: - model_class: User - david: - name: David + _fixture: + model_class: User + david: + name: David Fixes #9516. @@ -381,6 +450,13 @@ *Wojciech Wnętrzak* +* Instantiating an AR model with `ActionController::Parameters` now raises + an `ActiveModel::ForbiddenAttributesError` if the parameters include a + `type` field that has not been explicitly permitted. Previously, the + `type` field was simply ignored in the same situation. + + *Prem Sichanugrist* + * PostgreSQL, `create_schema`, `drop_schema` and `rename_table` now quote schema names. diff --git a/activerecord/README.rdoc b/activerecord/README.rdoc index 3eac8cc422..7eb4e9db1a 100644 --- a/activerecord/README.rdoc +++ b/activerecord/README.rdoc @@ -188,7 +188,7 @@ Admit the Database: The latest version of Active Record can be installed with RubyGems: - % gem install activerecord + $ gem install activerecord Source code can be downloaded as part of the Rails project on GitHub: @@ -215,4 +215,3 @@ Bug reports can be filed for the Ruby on Rails project here: Feature requests should be discussed on the rails-core mailing list here: * https://groups.google.com/forum/?fromgroups#!forum/rubyonrails-core - diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb index c7b396f3d4..d64ab64c99 100644 --- a/activerecord/lib/active_record/associations/association.rb +++ b/activerecord/lib/active_record/associations/association.rb @@ -163,9 +163,12 @@ module ActiveRecord @reflection = @owner.class._reflect_on_association(reflection_name) end - def initialize_attributes(record) #:nodoc: + def initialize_attributes(record, except_from_scope_attributes = nil) #:nodoc: + except_from_scope_attributes ||= {} skip_assign = [reflection.foreign_key, reflection.type].compact - attributes = create_scope.except(*(record.changed - skip_assign)) + assigned_keys = record.changed + assigned_keys += except_from_scope_attributes.keys.map(&:to_s) + attributes = create_scope.except(*(assigned_keys - skip_assign)) record.assign_attributes(attributes) set_inverse_instance(record) end @@ -248,7 +251,7 @@ module ActiveRecord def build_record(attributes) reflection.build_association(attributes) do |record| - initialize_attributes(record) + initialize_attributes(record, attributes) end end diff --git a/activerecord/lib/active_record/associations/builder/belongs_to.rb b/activerecord/lib/active_record/associations/builder/belongs_to.rb index dae468ba54..f02d146e89 100644 --- a/activerecord/lib/active_record/associations/builder/belongs_to.rb +++ b/activerecord/lib/active_record/associations/builder/belongs_to.rb @@ -106,8 +106,7 @@ module ActiveRecord::Associations::Builder # :nodoc: touch = reflection.options[:touch] callback = lambda { |record| - touch_method = touching_delayed_records? ? :touch : :touch_later - BelongsTo.touch_record(record, foreign_key, n, touch, touch_method) + BelongsTo.touch_record(record, foreign_key, n, touch, belongs_to_touch_method) } model.after_save callback, if: :changed? @@ -116,8 +115,7 @@ module ActiveRecord::Associations::Builder # :nodoc: end def self.add_destroy_callbacks(model, reflection) - name = reflection.name - model.after_destroy lambda { |o| o.association(name).handle_dependency } + model.after_destroy lambda { |o| o.association(reflection.name).handle_dependency } end def self.define_validations(model, reflection) diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index f32dddb8f0..473b80a658 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -414,12 +414,16 @@ module ActiveRecord def replace_on_target(record, index, skip_callbacks) callback(:before_add, record) unless skip_callbacks + + was_loaded = loaded? yield(record) if block_given? - if index - @target[index] = record - else - @target << record + unless !was_loaded && loaded? + if index + @target[index] = record + else + @target << record + end end callback(:after_add, record) unless skip_callbacks diff --git a/activerecord/lib/active_record/associations/join_dependency.rb b/activerecord/lib/active_record/associations/join_dependency.rb index 9f183c3e7e..0e4e951269 100644 --- a/activerecord/lib/active_record/associations/join_dependency.rb +++ b/activerecord/lib/active_record/associations/join_dependency.rb @@ -32,7 +32,7 @@ module ActiveRecord @alias_cache[node][column] end - class Table < Struct.new(:node, :columns) + class Table < Struct.new(:node, :columns) # :nodoc: def table Arel::Nodes::TableAlias.new node.table, node.aliased_table_name end diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index 4ae585d3f5..423a93964e 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -191,6 +191,18 @@ module ActiveRecord end end + # Returns true if the given attribute exists, otherwise false. + # + # class Person < ActiveRecord::Base + # end + # + # Person.has_attribute?('name') # => true + # Person.has_attribute?(:age) # => true + # Person.has_attribute?(:nothing) # => false + def has_attribute?(attr_name) + attribute_types.key?(attr_name.to_s) + end + # Returns the column object for the named attribute. # Returns a +ActiveRecord::ConnectionAdapters::NullColumn+ if the # named attribute does not exist. diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb index cfd8cbda67..4058affec3 100644 --- a/activerecord/lib/active_record/callbacks.rb +++ b/activerecord/lib/active_record/callbacks.rb @@ -208,12 +208,12 @@ module ActiveRecord # # Sometimes the code needs that the callbacks execute in a specific order. For example, a +before_destroy+ # callback (+log_children+ in this case) should be executed before the children get destroyed by the - # <tt>dependent: destroy</tt> option. + # <tt>dependent: :destroy</tt> option. # # Let's look at the code below: # # class Topic < ActiveRecord::Base - # has_many :children, dependent: destroy + # has_many :children, dependent: :destroy # # before_destroy :log_children # @@ -228,7 +228,7 @@ module ActiveRecord # You can use the +prepend+ option on the +before_destroy+ callback to avoid this. # # class Topic < ActiveRecord::Base - # has_many :children, dependent: destroy + # has_many :children, dependent: :destroy # # before_destroy :log_children, prepend: true # @@ -238,7 +238,7 @@ module ActiveRecord # end # end # - # This way, the +before_destroy+ gets executed before the <tt>dependent: destroy</tt> is called, and the data is still available. + # This way, the +before_destroy+ gets executed before the <tt>dependent: :destroy</tt> is called, and the data is still available. # # == \Transactions # 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 486b7b6d25..ccd2899489 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -197,7 +197,7 @@ module ActiveRecord elapsed = Time.now - t0 if elapsed >= timeout - msg = 'could not obtain a database connection within %0.3f seconds (waited %0.3f seconds)' % + msg = 'could not obtain a connection from the pool within %0.3f seconds (waited %0.3f seconds); all pooled connections were in use' % [timeout, elapsed] raise ConnectionTimeoutError, msg end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb index abf0124562..159cbcb85a 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -206,14 +206,13 @@ module ActiveRecord include ColumnMethods attr_accessor :indexes - attr_reader :name, :temporary, :options, :as, :foreign_keys, :native + attr_reader :name, :temporary, :options, :as, :foreign_keys - def initialize(types, name, temporary, options, as = nil) + def initialize(name, temporary, options, as = nil) @columns_hash = {} @indexes = {} @foreign_keys = {} @primary_keys = nil - @native = types @temporary = temporary @options = options @as = as @@ -362,11 +361,8 @@ module ActiveRecord def new_column_definition(name, type, options) # :nodoc: type = aliased_types(type.to_s, type) column = create_column_definition name, type - limit = options.fetch(:limit) do - native[type][:limit] if native[type].is_a?(Hash) - end - column.limit = limit + column.limit = options[:limit] column.precision = options[:precision] column.scale = options[:scale] column.default = options[:default] @@ -627,11 +623,6 @@ module ActiveRecord def foreign_key_exists?(*args) # :nodoc: @base.foreign_key_exists?(name, *args) end - - private - def native - @base.native_database_types - end end end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb index d5f8dbc8fc..b50d28862c 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -263,7 +263,7 @@ module ActiveRecord yield td if block_given? - if options[:force] && table_exists?(table_name) + if options[:force] && data_source_exists?(table_name) drop_table(table_name, options) end @@ -1088,7 +1088,7 @@ module ActiveRecord if index_name.length > max_index_length raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' is too long; the limit is #{max_index_length} characters" end - if table_exists?(table_name) && index_name_exists?(table_name, index_name, false) + if data_source_exists?(table_name) && index_name_exists?(table_name, index_name, false) raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' already exists" end index_columns = quoted_columns_for_index(column_names, options).join(", ") @@ -1168,7 +1168,7 @@ module ActiveRecord private def create_table_definition(name, temporary = false, options = nil, as = nil) - TableDefinition.new native_database_types, name, temporary, options, as + TableDefinition.new(name, temporary, options, as) end def create_alter_table(name) diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index 55910865e5..4b6912c616 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -95,14 +95,15 @@ module ActiveRecord attr_reader :prepared_statements - def initialize(connection, logger = nil, pool = nil) #:nodoc: + def initialize(connection, logger = nil, config = {}) # :nodoc: super() @connection = connection @owner = nil @instrumenter = ActiveSupport::Notifications.instrumenter @logger = logger - @pool = pool + @config = config + @pool = nil @schema_cache = SchemaCache.new self @visitor = nil @prepared_statements = false @@ -289,14 +290,14 @@ module ActiveRecord # locks # # Return true if we got the lock, otherwise false - def get_advisory_lock(key) # :nodoc: + def get_advisory_lock(lock_id) # :nodoc: end # This is meant to be implemented by the adapters that support advisory # locks. # # Return true if we released the lock, otherwise false - def release_advisory_lock(key) # :nodoc: + def release_advisory_lock(lock_id) # :nodoc: end # A list of extensions, to be filled in by adapters that support them. 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 3e3bbc267b..25ba42e5c9 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -134,9 +134,7 @@ module ActiveRecord time: { name: "time" }, date: { name: "date" }, binary: { name: "blob" }, - blob: { name: "blob" }, boolean: { name: "tinyint", limit: 1 }, - bigint: { name: "bigint" }, json: { name: "json" }, } @@ -145,8 +143,7 @@ module ActiveRecord # FIXME: Make the first parameter more similar for the two adapters def initialize(connection, logger, connection_options, config) - super(connection, logger) - @connection_options, @config = connection_options, config + super(connection, logger, config) @quoted_column_names, @quoted_table_names = {}, {} @visitor = Arel::Visitors::MySQL.new self @@ -226,12 +223,12 @@ module ActiveRecord version >= '5.0.0' end - def get_advisory_lock(key, timeout = 0) # :nodoc: - select_value("SELECT GET_LOCK('#{key}', #{timeout});").to_s == '1' + def get_advisory_lock(lock_name, timeout = 0) # :nodoc: + select_value("SELECT GET_LOCK('#{lock_name}', #{timeout});").to_s == '1' end - def release_advisory_lock(key) # :nodoc: - select_value("SELECT RELEASE_LOCK('#{key}')").to_s == '1' + def release_advisory_lock(lock_name) # :nodoc: + select_value("SELECT RELEASE_LOCK('#{lock_name}')").to_s == '1' end def native_database_types @@ -497,18 +494,43 @@ module ActiveRecord end def tables(name = nil) # :nodoc: + ActiveSupport::Deprecation.warn(<<-MSG.squish) + #tables currently returns both tables and views. + This behavior is deprecated and will be changed with Rails 5.1 to only return tables. + Use #data_sources instead. + MSG + + if name + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Passing arguments to #tables is deprecated without replacement. + MSG + end + + data_sources + end + + def data_sources sql = "SELECT table_name FROM information_schema.tables " sql << "WHERE table_schema = #{quote(@config[:database])}" select_values(sql, 'SCHEMA') end - alias data_sources tables def truncate(table_name, name = nil) execute "TRUNCATE TABLE #{quote_table_name(table_name)}", name end def table_exists?(table_name) + ActiveSupport::Deprecation.warn(<<-MSG.squish) + #table_exists? currently checks both tables and views. + This behavior is deprecated and will be changed with Rails 5.1 to only check tables. + Use #data_source_exists? instead. + MSG + + data_source_exists?(table_name) + end + + def data_source_exists?(table_name) return false unless table_name.present? schema, name = table_name.to_s.split('.', 2) @@ -519,7 +541,6 @@ module ActiveRecord select_values(sql, 'SCHEMA').any? end - alias data_source_exists? table_exists? def views # :nodoc: select_values("SHOW FULL TABLES WHERE table_type = 'VIEW'", 'SCHEMA') @@ -566,10 +587,8 @@ module ActiveRecord sql = "SHOW FULL FIELDS FROM #{quote_table_name(table_name)}" execute_and_free(sql, 'SCHEMA') do |result| each_hash(result).map do |field| - field_name = set_field_encoding(field[:Field]) - sql_type = field[:Type] - type_metadata = fetch_type_metadata(sql_type, field[:Extra]) - new_column(field_name, field[:Default], type_metadata, field[:Null] == "YES", nil, field[:Collation]) + type_metadata = fetch_type_metadata(field[:Type], field[:Extra]) + new_column(field[:Field], field[:Default], type_metadata, field[:Null] == "YES", nil, field[:Collation]) end end end @@ -1019,7 +1038,7 @@ module ActiveRecord end def create_table_definition(name, temporary = false, options = nil, as = nil) # :nodoc: - MySQL::TableDefinition.new(native_database_types, name, temporary, options, as) + MySQL::TableDefinition.new(name, temporary, options, as) end def integer_to_sql(limit) # :nodoc: diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb index 773ecbe126..7ca597859d 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -16,8 +16,7 @@ module ActiveRecord end client = Mysql2::Client.new(config) - options = [config[:host], config[:username], config[:password], config[:database], config[:port], config[:socket], 0] - ConnectionAdapters::Mysql2Adapter.new(client, logger, options, config) + ConnectionAdapters::Mysql2Adapter.new(client, logger, nil, config) rescue Mysql2::Error => error if error.message.include?("Unknown database") raise ActiveRecord::NoDatabaseError @@ -185,10 +184,6 @@ module ActiveRecord def full_version @full_version ||= @connection.server_info[:version] end - - def set_field_encoding field_name - field_name - end end end end diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb index 89d18ee14e..76f1b91e6b 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb @@ -82,6 +82,7 @@ module ActiveRecord super @statements = StatementPool.new(self.class.type_cast_config_to_integer(config.fetch(:statement_limit) { 1000 })) @client_encoding = nil + @connection_options = connection_options connect end @@ -104,6 +105,11 @@ module ActiveRecord end end + def new_column(field, default, sql_type_metadata = nil, null = true, default_function = nil, collation = nil) # :nodoc: + field = set_field_encoding(field) + super + end + def error_number(exception) # :nodoc: exception.errno if exception.respond_to?(:errno) end @@ -463,7 +469,7 @@ module ActiveRecord @full_version ||= @connection.server_info end - def set_field_encoding field_name + def set_field_encoding(field_name) field_name.force_encoding(client_encoding) if internal_enc = Encoding.default_internal field_name = field_name.encode!(internal_enc) 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 aaf5b2898b..a48d64f7bd 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -70,6 +70,12 @@ module ActiveRecord # Returns the list of all tables in the schema search path. def tables(name = nil) + if name + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Passing arguments to #tables is deprecated without replacement. + MSG + end + select_values("SELECT tablename FROM pg_tables WHERE schemaname = ANY(current_schemas(false))", 'SCHEMA') end @@ -87,6 +93,16 @@ module ActiveRecord # If the schema is not specified as part of +name+ then it will only find tables within # the current schema search path (regardless of permissions to access tables in other schemas) def table_exists?(name) + ActiveSupport::Deprecation.warn(<<-MSG.squish) + #table_exists? currently checks both tables and views. + This behavior is deprecated and will be changed with Rails 5.1 to only check tables. + Use #data_source_exists? instead. + MSG + + data_source_exists?(name) + end + + def data_source_exists?(name) name = Utils.extract_schema_qualified_name(name.to_s) return false unless name.identifier @@ -99,7 +115,6 @@ module ActiveRecord AND n.nspname = #{name.schema ? "'#{name.schema}'" : 'ANY (current_schemas(false))'} SQL end - alias data_source_exists? table_exists? def views # :nodoc: select_values(<<-SQL, 'SCHEMA') diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index ed6ab8235f..aa43854d01 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -72,7 +72,6 @@ module ActiveRecord NATIVE_DATABASE_TYPES = { primary_key: "serial primary key", - bigserial: "bigserial", string: { name: "character varying" }, text: { name: "text" }, integer: { name: "integer" }, @@ -89,7 +88,6 @@ module ActiveRecord int8range: { name: "int8range" }, binary: { name: "bytea" }, boolean: { name: "boolean" }, - bigint: { name: "bigint" }, xml: { name: "xml" }, tsvector: { name: "tsvector" }, hstore: { name: "hstore" }, @@ -102,6 +100,12 @@ module ActiveRecord ltree: { name: "ltree" }, citext: { name: "citext" }, point: { name: "point" }, + line: { name: "line" }, + lseg: { name: "lseg" }, + box: { name: "box" }, + path: { name: "path" }, + polygon: { name: "polygon" }, + circle: { name: "circle" }, bit: { name: "bit" }, bit_varying: { name: "bit varying" }, money: { name: "money" }, @@ -188,7 +192,7 @@ module ActiveRecord # Initializes and connects a PostgreSQL adapter. def initialize(connection, logger, connection_parameters, config) - super(connection, logger) + super(connection, logger, config) @visitor = Arel::Visitors::PostgreSQL.new self if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true }) @@ -198,7 +202,7 @@ module ActiveRecord @prepared_statements = false end - @connection_parameters, @config = connection_parameters, config + @connection_parameters = connection_parameters # @local_tz is initialized as nil to avoid warnings when connect tries to use it @local_tz = nil @@ -306,18 +310,18 @@ module ActiveRecord postgresql_version >= 90300 end - def get_advisory_lock(key) # :nodoc: - unless key.is_a?(Integer) && key.bit_length <= 63 - raise(ArgumentError, "Postgres requires advisory lock keys to be a signed 64 bit integer") + def get_advisory_lock(lock_id) # :nodoc: + unless lock_id.is_a?(Integer) && lock_id.bit_length <= 63 + raise(ArgumentError, "Postgres requires advisory lock ids to be a signed 64 bit integer") end - select_value("SELECT pg_try_advisory_lock(#{key});") + select_value("SELECT pg_try_advisory_lock(#{lock_id});") end - def release_advisory_lock(key) # :nodoc: - unless key.is_a?(Integer) && key.bit_length <= 63 - raise(ArgumentError, "Postgres requires advisory lock keys to be a signed 64 bit integer") + def release_advisory_lock(lock_id) # :nodoc: + unless lock_id.is_a?(Integer) && lock_id.bit_length <= 63 + raise(ArgumentError, "Postgres requires advisory lock ids to be a signed 64 bit integer") end - select_value("SELECT pg_advisory_unlock(#{key})") + select_value("SELECT pg_advisory_unlock(#{lock_id})") end def enable_extension(name) @@ -457,15 +461,15 @@ module ActiveRecord m.register_type 'macaddr', OID::SpecializedString.new(:macaddr) m.register_type 'citext', OID::SpecializedString.new(:citext) m.register_type 'ltree', OID::SpecializedString.new(:ltree) + m.register_type 'line', OID::SpecializedString.new(:line) + m.register_type 'lseg', OID::SpecializedString.new(:lseg) + m.register_type 'box', OID::SpecializedString.new(:box) + m.register_type 'path', OID::SpecializedString.new(:path) + m.register_type 'polygon', OID::SpecializedString.new(:polygon) + m.register_type 'circle', OID::SpecializedString.new(:circle) # FIXME: why are we keeping these types as strings? m.alias_type 'interval', 'varchar' - m.alias_type 'path', 'varchar' - m.alias_type 'line', 'varchar' - m.alias_type 'polygon', 'varchar' - m.alias_type 'circle', 'varchar' - m.alias_type 'lseg', 'varchar' - m.alias_type 'box', 'varchar' register_class_with_precision m, 'time', Type::Time register_class_with_precision m, 'timestamp', OID::DateTime @@ -736,7 +740,7 @@ module ActiveRecord end def create_table_definition(name, temporary = false, options = nil, as = nil) # :nodoc: - PostgreSQL::TableDefinition.new native_database_types, name, temporary, options, as + PostgreSQL::TableDefinition.new(name, temporary, options, as) end def can_perform_case_insensitive_comparison_for?(column) diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index 32fe275bfb..163cbb875f 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -78,11 +78,10 @@ module ActiveRecord end def initialize(connection, logger, connection_options, config) - super(connection, logger) + super(connection, logger, config) @active = nil @statements = StatementPool.new(self.class.type_cast_config_to_integer(config.fetch(:statement_limit) { 1000 })) - @config = config @visitor = Arel::Visitors::SQLite.new self @quoted_column_names = {} @@ -130,6 +129,10 @@ module ActiveRecord true end + def supports_datetime_with_precision? + true + end + def active? @active != false end @@ -312,11 +315,36 @@ module ActiveRecord # SCHEMA STATEMENTS ======================================== def tables(name = nil) # :nodoc: + ActiveSupport::Deprecation.warn(<<-MSG.squish) + #tables currently returns both tables and views. + This behavior is deprecated and will be changed with Rails 5.1 to only return tables. + Use #data_sources instead. + MSG + + if name + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Passing arguments to #tables is deprecated without replacement. + MSG + end + + data_sources + end + + def data_sources select_values("SELECT name FROM sqlite_master WHERE type IN ('table','view') AND name <> 'sqlite_sequence'", 'SCHEMA') end - alias data_sources tables def table_exists?(table_name) + ActiveSupport::Deprecation.warn(<<-MSG.squish) + #table_exists? currently checks both tables and views. + This behavior is deprecated and will be changed with Rails 5.1 to only check tables. + Use #data_source_exists? instead. + MSG + + data_source_exists?(table_name) + end + + def data_source_exists?(table_name) return false unless table_name.present? sql = "SELECT name FROM sqlite_master WHERE type IN ('table','view') AND name <> 'sqlite_sequence'" @@ -324,7 +352,6 @@ module ActiveRecord select_values(sql, 'SCHEMA').any? end - alias data_source_exists? table_exists? def views # :nodoc: select_values("SELECT name FROM sqlite_master WHERE type = 'view' AND name <> 'sqlite_sequence'", 'SCHEMA') diff --git a/activerecord/lib/active_record/enum.rb b/activerecord/lib/active_record/enum.rb index 8fba6fcc35..7ded96f8fb 100644 --- a/activerecord/lib/active_record/enum.rb +++ b/activerecord/lib/active_record/enum.rb @@ -104,7 +104,7 @@ module ActiveRecord super end - class EnumType < Type::Value + class EnumType < Type::Value # :nodoc: def initialize(name, mapping) @name = name @mapping = mapping diff --git a/activerecord/lib/active_record/inheritance.rb b/activerecord/lib/active_record/inheritance.rb index 589c70db0d..6259c4cd33 100644 --- a/activerecord/lib/active_record/inheritance.rb +++ b/activerecord/lib/active_record/inheritance.rb @@ -51,8 +51,8 @@ module ActiveRecord end attrs = args.first - if subclass_from_attributes?(attrs) - subclass = subclass_from_attributes(attrs) + if has_attribute?(inheritance_column) + subclass = subclass_from_attributes(attrs) || subclass_from_attributes(column_defaults) end if subclass && subclass != self @@ -163,7 +163,7 @@ module ActiveRecord end def using_single_table_inheritance?(record) - record[inheritance_column].present? && columns_hash.include?(inheritance_column) + record[inheritance_column].present? && has_attribute?(inheritance_column) end def find_sti_class(type_name) @@ -195,17 +195,14 @@ module ActiveRecord # Detect the subclass from the inheritance column of attrs. If the inheritance column value # is not self or a valid subclass, raises ActiveRecord::SubclassNotFound - # If this is a StrongParameters hash, and access to inheritance_column is not permitted, - # this will ignore the inheritance column and return nil - def subclass_from_attributes?(attrs) - attribute_names.include?(inheritance_column) && attrs.is_a?(Hash) - end - def subclass_from_attributes(attrs) - subclass_name = attrs.with_indifferent_access[inheritance_column] + attrs = attrs.to_h if attrs.respond_to?(:permitted?) + if attrs.is_a?(Hash) + subclass_name = attrs.with_indifferent_access[inheritance_column] - if subclass_name.present? - find_sti_class(subclass_name) + if subclass_name.present? + find_sti_class(subclass_name) + end end end end diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index 1e870d4c44..ca2537cdc3 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -967,10 +967,12 @@ module ActiveRecord end def get_all_versions(connection = Base.connection) - if connection.table_exists?(schema_migrations_table_name) - SchemaMigration.all.map { |x| x.version.to_i }.sort - else - [] + ActiveSupport::Deprecation.silence do + if connection.table_exists?(schema_migrations_table_name) + SchemaMigration.all.map { |x| x.version.to_i }.sort + else + [] + end end end @@ -1200,17 +1202,17 @@ module ActiveRecord end def with_advisory_lock - key = generate_migrator_advisory_lock_key - got_lock = Base.connection.get_advisory_lock(key) + lock_id = generate_migrator_advisory_lock_id + got_lock = Base.connection.get_advisory_lock(lock_id) raise ConcurrentMigrationError unless got_lock load_migrated # reload schema_migrations to be sure it wasn't changed by another process before we got the lock yield ensure - Base.connection.release_advisory_lock(key) if got_lock + Base.connection.release_advisory_lock(lock_id) if got_lock end MIGRATOR_SALT = 2053462845 - def generate_migrator_advisory_lock_key + def generate_migrator_advisory_lock_id db_name_hash = Zlib.crc32(Base.connection.current_database) MIGRATOR_SALT * db_name_hash end diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb index a9bd094a66..e3f304b0af 100644 --- a/activerecord/lib/active_record/model_schema.rb +++ b/activerecord/lib/active_record/model_schema.rb @@ -339,6 +339,9 @@ module ActiveRecord @columns = nil @columns_hash = nil @attribute_names = nil + direct_descendants.each do |descendant| + descendant.send(:reload_schema_from_cache) + end end # Guesses the table name, but does not decorate it with prefix and suffix information. diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index 94316d5249..9e566031b8 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -298,6 +298,7 @@ module ActiveRecord # * \Validations are skipped. # * \Callbacks are skipped. # * +updated_at+/+updated_on+ are not updated. + # * However, attributes are serialized with the same rules as ActiveRecord::Relation#update_all # # This method raises an ActiveRecord::ActiveRecordError when called on new # objects, or when at least one of the attributes is marked as readonly. @@ -358,6 +359,14 @@ module ActiveRecord # if the predicate returns +true+ the attribute will become +false+. This # method toggles directly the underlying value without calling any setter. # Returns +self+. + # + # Example: + # + # user = User.first + # user.banned? # => false + # user.toggle(:banned) + # user.banned? # => true + # def toggle(attribute) self[attribute] = !public_send("#{attribute}?") self @@ -558,5 +567,9 @@ module ActiveRecord ensure @_association_destroy_exception = nil end + + def belongs_to_touch_method + :touch + end end end diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index 5b9d45d871..a549b28f16 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -371,7 +371,7 @@ module ActiveRecord end def foreign_key - @foreign_key ||= options[:foreign_key] || derive_foreign_key + @foreign_key ||= options[:foreign_key] || derive_foreign_key.freeze end def association_foreign_key diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index f100476374..2cf19c76c5 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -347,9 +347,8 @@ module ActiveRecord # Updates all records in the current relation with details given. This method constructs a single SQL UPDATE # statement and sends it straight to the database. It does not instantiate the involved models and it does not - # trigger Active Record callbacks or validations. Values passed to #update_all will not go through - # Active Record's type-casting behavior. It should receive only values that can be passed as-is to the SQL - # database. + # trigger Active Record callbacks or validations. However, values passed to #update_all will still go through + # Active Record's normal type casting and serialization. # # ==== Parameters # diff --git a/activerecord/lib/active_record/relation/delegation.rb b/activerecord/lib/active_record/relation/delegation.rb index 27de313d05..e4e5d63006 100644 --- a/activerecord/lib/active_record/relation/delegation.rb +++ b/activerecord/lib/active_record/relation/delegation.rb @@ -36,13 +36,8 @@ module ActiveRecord # may vary depending on the klass of a relation, so we create a subclass of Relation # for each different klass, and the delegations are compiled into that subclass only. - BLACKLISTED_ARRAY_METHODS = [ - :compact!, :flatten!, :reject!, :reverse!, :rotate!, :map!, - :shuffle!, :slice!, :sort!, :sort_by!, :delete_if, - :keep_if, :pop, :shift, :delete_at, :select! - ].to_set # :nodoc: - - delegate :to_xml, :to_yaml, :length, :collect, :map, :each, :all?, :include?, :to_ary, :join, to: :to_a + delegate :to_xml, :to_yaml, :length, :collect, :map, :each, :all?, :include?, :to_ary, :join, + :[], :&, :|, :+, :-, :sample, :shuffle, :reverse, :compact, to: :to_a delegate :table_name, :quoted_table_name, :primary_key, :quoted_primary_key, :connection, :columns_hash, :to => :klass @@ -114,21 +109,14 @@ module ActiveRecord def respond_to?(method, include_private = false) super || @klass.respond_to?(method, include_private) || - array_delegable?(method) || arel.respond_to?(method, include_private) end protected - def array_delegable?(method) - Array.method_defined?(method) && BLACKLISTED_ARRAY_METHODS.exclude?(method) - end - def method_missing(method, *args, &block) if @klass.respond_to?(method) scoping { @klass.public_send(method, *args, &block) } - elsif array_delegable?(method) - to_a.public_send(method, *args, &block) elsif arel.respond_to?(method) arel.public_send(method, *args, &block) else diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 2dc52982c9..dbecb842b5 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -13,6 +13,8 @@ module ActiveRecord # WhereChain objects act as placeholder for queries in which #where does not have any parameter. # In this case, #where must be chained with #not to return a new relation. class WhereChain + include ActiveModel::ForbiddenAttributesProtection + def initialize(scope) @scope = scope end @@ -41,6 +43,8 @@ module ActiveRecord # User.where.not(name: "Jon", role: "admin") # # SELECT * FROM users WHERE name != 'Jon' AND role != 'admin' def not(opts, *rest) + opts = sanitize_forbidden_attributes(opts) + where_clause = @scope.send(:where_clause_factory).build(opts, rest) @scope.references!(PredicateBuilder.references(opts)) if Hash === opts @@ -407,10 +411,30 @@ module ActiveRecord self end - # Performs a joins on +args+: + # Performs a joins on +args+. The given symbol(s) should match the name of + # the association(s). # # User.joins(:posts) - # # SELECT "users".* FROM "users" INNER JOIN "posts" ON "posts"."user_id" = "users"."id" + # # SELECT "users".* + # # FROM "users" + # # INNER JOIN "posts" ON "posts"."user_id" = "users"."id" + # + # Multiple joins: + # + # User.joins(:posts, :account) + # # SELECT "users".* + # # FROM "users" + # # INNER JOIN "posts" ON "posts"."user_id" = "users"."id" + # # INNER JOIN "accounts" ON "accounts"."id" = "users"."account_id" + # + # Nested joins: + # + # User.joins(posts: [:comments]) + # # SELECT "users".* + # # FROM "users" + # # INNER JOIN "posts" ON "posts"."user_id" = "users"."id" + # # INNER JOIN "comments" "comments_posts" + # # ON "comments_posts"."post_id" = "posts"."id" # # You can use strings in order to customize your joins: # diff --git a/activerecord/lib/active_record/relation/record_fetch_warning.rb b/activerecord/lib/active_record/relation/record_fetch_warning.rb index 14e1bf89fa..0a1814b3dd 100644 --- a/activerecord/lib/active_record/relation/record_fetch_warning.rb +++ b/activerecord/lib/active_record/relation/record_fetch_warning.rb @@ -23,11 +23,13 @@ module ActiveRecord end end + # :stopdoc: ActiveSupport::Notifications.subscribe("sql.active_record") do |*args| payload = args.last QueryRegistry.queries << payload[:sql] end + # :startdoc: class QueryRegistry # :nodoc: extend ActiveSupport::PerThreadRegistry diff --git a/activerecord/lib/active_record/relation/spawn_methods.rb b/activerecord/lib/active_record/relation/spawn_methods.rb index 5c3318651a..67d7f83cb4 100644 --- a/activerecord/lib/active_record/relation/spawn_methods.rb +++ b/activerecord/lib/active_record/relation/spawn_methods.rb @@ -12,6 +12,7 @@ module ActiveRecord # Merges in the conditions from <tt>other</tt>, if <tt>other</tt> is an ActiveRecord::Relation. # Returns an array representing the intersection of the resulting records with <tt>other</tt>, if <tt>other</tt> is an array. + # # Post.where(published: true).joins(:comments).merge( Comment.where(spam: false) ) # # Performs a single join query with both where conditions. # @@ -37,11 +38,14 @@ module ActiveRecord end def merge!(other) # :nodoc: - if !other.is_a?(Relation) && other.respond_to?(:to_proc) + if other.is_a?(Hash) + Relation::HashMerger.new(self, other).merge + elsif other.is_a?(Relation) + Relation::Merger.new(self, other).merge + elsif other.respond_to?(:to_proc) instance_exec(&other) else - klass = other.is_a?(Hash) ? Relation::HashMerger : Relation::Merger - klass.new(self, other).merge + raise ArgumentError, "#{other.inspect} is not an ActiveRecord::Relation" end end diff --git a/activerecord/lib/active_record/schema_migration.rb b/activerecord/lib/active_record/schema_migration.rb index b384529e75..51b9b17395 100644 --- a/activerecord/lib/active_record/schema_migration.rb +++ b/activerecord/lib/active_record/schema_migration.rb @@ -21,7 +21,7 @@ module ActiveRecord end def table_exists? - connection.table_exists?(table_name) + ActiveSupport::Deprecation.silence { connection.table_exists?(table_name) } end def create_table(limit=nil) diff --git a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb index cd7d949239..8b4874044c 100644 --- a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb +++ b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb @@ -56,9 +56,9 @@ module ActiveRecord args = ['-s', '-x', '-O', '-f', filename] unless search_path.blank? - args << search_path.split(',').map do |part| + args += search_path.split(',').map do |part| "--schema=#{part.strip}" - end.join(' ') + end end args << configuration['database'] run_cmd('pg_dump', args, 'dumping') diff --git a/activerecord/lib/active_record/touch_later.rb b/activerecord/lib/active_record/touch_later.rb index 4352a0ffea..9a80a63e28 100644 --- a/activerecord/lib/active_record/touch_later.rb +++ b/activerecord/lib/active_record/touch_later.rb @@ -16,6 +16,13 @@ module ActiveRecord surreptitiously_touch @_defer_touch_attrs self.class.connection.add_transaction_record self + + # touch the parents as we are not calling the after_save callbacks + self.class.reflect_on_all_associations(:belongs_to).each do |r| + if touch = r.options[:touch] + ActiveRecord::Associations::Builder::BelongsTo.touch_record(self, r.foreign_key, r.name, touch, :touch_later) + end + end end def touch(*names, time: nil) # :nodoc: @@ -26,6 +33,7 @@ module ActiveRecord end private + def surreptitiously_touch(attrs) attrs.each { |attr| write_attribute attr, @_touch_time } clear_attribute_changes attrs @@ -33,9 +41,8 @@ module ActiveRecord 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 + @_defer_touch_attrs, @_touch_time = nil, nil end end @@ -43,8 +50,9 @@ module ActiveRecord defined?(@_defer_touch_attrs) && @_defer_touch_attrs.present? end - def touching_delayed_records? - defined?(@_touching_delayed_records) && @_touching_delayed_records + def belongs_to_touch_method + :touch_later end + end end diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb index 8de82feae3..38ab1f3fc6 100644 --- a/activerecord/lib/active_record/transactions.rb +++ b/activerecord/lib/active_record/transactions.rb @@ -233,6 +233,24 @@ module ActiveRecord set_callback(:commit, :after, *args, &block) end + # Shortcut for +after_commit :hook, on: :create+. + def after_create_commit(*args, &block) + set_options_for_callbacks!(args, on: :create) + set_callback(:commit, :after, *args, &block) + end + + # Shortcut for +after_commit :hook, on: :update+. + def after_update_commit(*args, &block) + set_options_for_callbacks!(args, on: :update) + set_callback(:commit, :after, *args, &block) + end + + # Shortcut for +after_commit :hook, on: :destroy+. + def after_destroy_commit(*args, &block) + set_options_for_callbacks!(args, on: :destroy) + set_callback(:commit, :after, *args, &block) + end + # This callback is called after a create, update, or destroy are rolled back. # # Please check the documentation of #after_commit for options. @@ -268,9 +286,11 @@ module ActiveRecord private - def set_options_for_callbacks!(args) - options = args.last - if options.is_a?(Hash) && options[:on] + def set_options_for_callbacks!(args, enforced_options = {}) + options = args.extract_options!.merge!(enforced_options) + args << options + + if options[:on] fire_on = Array(options[:on]) assert_valid_transaction_action(fire_on) options[:if] = Array(options[:if]) diff --git a/activerecord/lib/active_record/type_caster.rb b/activerecord/lib/active_record/type_caster.rb index 63ba10c289..accc339d00 100644 --- a/activerecord/lib/active_record/type_caster.rb +++ b/activerecord/lib/active_record/type_caster.rb @@ -2,6 +2,6 @@ require 'active_record/type_caster/map' require 'active_record/type_caster/connection' module ActiveRecord - module TypeCaster + module TypeCaster # :nodoc: end end diff --git a/activerecord/lib/active_record/type_caster/connection.rb b/activerecord/lib/active_record/type_caster/connection.rb index 868d08ed44..7ed8dcc313 100644 --- a/activerecord/lib/active_record/type_caster/connection.rb +++ b/activerecord/lib/active_record/type_caster/connection.rb @@ -1,6 +1,6 @@ module ActiveRecord module TypeCaster - class Connection + class Connection # :nodoc: def initialize(klass, table_name) @klass = klass @table_name = table_name diff --git a/activerecord/lib/active_record/type_caster/map.rb b/activerecord/lib/active_record/type_caster/map.rb index 4b1941351c..3a367b3999 100644 --- a/activerecord/lib/active_record/type_caster/map.rb +++ b/activerecord/lib/active_record/type_caster/map.rb @@ -1,6 +1,6 @@ module ActiveRecord module TypeCaster - class Map + class Map # :nodoc: def initialize(types) @types = types end diff --git a/activerecord/lib/active_record/validations/associated.rb b/activerecord/lib/active_record/validations/associated.rb index 32fbaf0a91..b14db85167 100644 --- a/activerecord/lib/active_record/validations/associated.rb +++ b/activerecord/lib/active_record/validations/associated.rb @@ -2,10 +2,16 @@ module ActiveRecord module Validations class AssociatedValidator < ActiveModel::EachValidator #:nodoc: def validate_each(record, attribute, value) - if Array.wrap(value).reject {|r| r.marked_for_destruction? || r.valid?}.any? - record.errors.add(attribute, :invalid, options.merge(:value => value)) + if Array(value).reject { |r| valid_object?(r) }.any? + record.errors.add(attribute, :invalid, options.merge(value: value)) end end + + private + + def valid_object?(record) + (record.respond_to?(:marked_for_destruction?) && record.marked_for_destruction?) || record.valid? + end end module ClassMethods diff --git a/activerecord/test/cases/adapter_test.rb b/activerecord/test/cases/adapter_test.rb index 77c47f12d5..abe1ea7c90 100644 --- a/activerecord/test/cases/adapter_test.rb +++ b/activerecord/test/cases/adapter_test.rb @@ -23,7 +23,8 @@ module ActiveRecord end def test_tables - tables = @connection.tables + tables = nil + ActiveSupport::Deprecation.silence { tables = @connection.tables } assert tables.include?("accounts") assert tables.include?("authors") assert tables.include?("tasks") @@ -31,9 +32,15 @@ module ActiveRecord end def test_table_exists? - assert @connection.table_exists?("accounts") - assert !@connection.table_exists?("nonexistingtable") - assert !@connection.table_exists?(nil) + ActiveSupport::Deprecation.silence do + assert @connection.table_exists?("accounts") + assert !@connection.table_exists?("nonexistingtable") + assert !@connection.table_exists?(nil) + end + end + + def test_table_exists_checking_both_tables_and_views_is_deprecated + assert_deprecated { @connection.table_exists?("accounts") } end def test_data_sources @@ -246,6 +253,16 @@ module ActiveRecord assert_not_nil error.cause end end + + if current_adapter?(:MysqlAdapter, :Mysql2Adapter, :SQLite3Adapter) + def test_tables_returning_both_tables_and_views_is_deprecated + assert_deprecated { @connection.tables } + end + end + + def test_passing_arguments_to_tables_is_deprecated + assert_deprecated { @connection.tables(:books) } + end end class AdapterTestWithoutTransaction < ActiveRecord::TestCase diff --git a/activerecord/test/cases/adapters/mysql/active_schema_test.rb b/activerecord/test/cases/adapters/mysql/active_schema_test.rb index 0b5c9e1798..8a7a0bb25d 100644 --- a/activerecord/test/cases/adapters/mysql/active_schema_test.rb +++ b/activerecord/test/cases/adapters/mysql/active_schema_test.rb @@ -16,8 +16,8 @@ class MysqlActiveSchemaTest < ActiveRecord::MysqlTestCase end def test_add_index - # add_index calls table_exists? and index_name_exists? which can't work since execute is stubbed - def (ActiveRecord::Base.connection).table_exists?(*); true; end + # add_index calls data_source_exists? and index_name_exists? which can't work since execute is stubbed + def (ActiveRecord::Base.connection).data_source_exists?(*); true; end def (ActiveRecord::Base.connection).index_name_exists?(*); false; end expected = "CREATE INDEX `index_people_on_last_name` ON `people` (`last_name`) " @@ -60,7 +60,7 @@ class MysqlActiveSchemaTest < ActiveRecord::MysqlTestCase end def test_index_in_create - def (ActiveRecord::Base.connection).table_exists?(*); false; end + def (ActiveRecord::Base.connection).data_source_exists?(*); false; end %w(SPATIAL FULLTEXT UNIQUE).each do |type| expected = "CREATE TABLE `people` (#{type} INDEX `index_people_on_last_name` (`last_name`) ) ENGINE=InnoDB" @@ -78,7 +78,7 @@ class MysqlActiveSchemaTest < ActiveRecord::MysqlTestCase end def test_index_in_bulk_change - def (ActiveRecord::Base.connection).table_exists?(*); true; end + def (ActiveRecord::Base.connection).data_source_exists?(*); true; end def (ActiveRecord::Base.connection).index_name_exists?(*); false; end %w(SPATIAL FULLTEXT UNIQUE).each do |type| @@ -152,7 +152,7 @@ class MysqlActiveSchemaTest < ActiveRecord::MysqlTestCase end def test_indexes_in_create - ActiveRecord::Base.connection.stubs(:table_exists?).with(:temp).returns(false) + ActiveRecord::Base.connection.stubs(:data_source_exists?).with(:temp).returns(false) ActiveRecord::Base.connection.stubs(:index_name_exists?).with(:index_temp_on_zip).returns(false) expected = "CREATE TEMPORARY TABLE `temp` ( INDEX `index_temp_on_zip` (`zip`) ) ENGINE=InnoDB AS SELECT id, name, zip FROM a_really_complicated_query" diff --git a/activerecord/test/cases/adapters/mysql/connection_test.rb b/activerecord/test/cases/adapters/mysql/connection_test.rb index 75653ee9af..390dd15b92 100644 --- a/activerecord/test/cases/adapters/mysql/connection_test.rb +++ b/activerecord/test/cases/adapters/mysql/connection_test.rb @@ -171,31 +171,31 @@ class MysqlConnectionTest < ActiveRecord::MysqlTestCase end def test_get_and_release_advisory_lock - key = "test_key" + lock_name = "test_lock_name" - got_lock = @connection.get_advisory_lock(key) + got_lock = @connection.get_advisory_lock(lock_name) assert got_lock, "get_advisory_lock should have returned true but it didn't" - assert_equal test_lock_free(key), false, + assert_equal test_lock_free(lock_name), false, "expected the test advisory lock to be held but it wasn't" - released_lock = @connection.release_advisory_lock(key) + released_lock = @connection.release_advisory_lock(lock_name) assert released_lock, "expected release_advisory_lock to return true but it didn't" - assert test_lock_free(key), 'expected the test key to be available after releasing' + assert test_lock_free(lock_name), 'expected the test lock to be available after releasing' end def test_release_non_existent_advisory_lock - fake_key = "fake_key" - released_non_existent_lock = @connection.release_advisory_lock(fake_key) + lock_name = "fake_lock_name" + released_non_existent_lock = @connection.release_advisory_lock(lock_name) assert_equal released_non_existent_lock, false, 'expected release_advisory_lock to return false when there was no lock to release' end protected - def test_lock_free(key) - @connection.select_value("SELECT IS_FREE_LOCK('#{key}');") == '1' + def test_lock_free(lock_name) + @connection.select_value("SELECT IS_FREE_LOCK('#{lock_name}');") == '1' end private diff --git a/activerecord/test/cases/adapters/mysql/schema_test.rb b/activerecord/test/cases/adapters/mysql/schema_test.rb index 04bcf0d839..14dbdd375b 100644 --- a/activerecord/test/cases/adapters/mysql/schema_test.rb +++ b/activerecord/test/cases/adapters/mysql/schema_test.rb @@ -59,13 +59,13 @@ module ActiveRecord assert_equal 'id', @omgpost.primary_key end - def test_table_exists? + def test_data_source_exists? name = @omgpost.table_name - assert @connection.table_exists?(name), "#{name} table should exist" + assert @connection.data_source_exists?(name), "#{name} data_source should exist" end - def test_table_exists_wrong_schema - assert(!@connection.table_exists?("#{@db_name}.zomg"), "table should not exist") + def test_data_source_exists_wrong_schema + assert(!@connection.data_source_exists?("#{@db_name}.zomg"), "data_source should not exist") end def test_dump_indexes diff --git a/activerecord/test/cases/adapters/mysql2/active_schema_test.rb b/activerecord/test/cases/adapters/mysql2/active_schema_test.rb index 31dc69a45b..99f97c7914 100644 --- a/activerecord/test/cases/adapters/mysql2/active_schema_test.rb +++ b/activerecord/test/cases/adapters/mysql2/active_schema_test.rb @@ -16,8 +16,8 @@ class Mysql2ActiveSchemaTest < ActiveRecord::Mysql2TestCase end def test_add_index - # add_index calls table_exists? and index_name_exists? which can't work since execute is stubbed - def (ActiveRecord::Base.connection).table_exists?(*); true; end + # add_index calls data_source_exists? and index_name_exists? which can't work since execute is stubbed + def (ActiveRecord::Base.connection).data_source_exists?(*); true; end def (ActiveRecord::Base.connection).index_name_exists?(*); false; end expected = "CREATE INDEX `index_people_on_last_name` ON `people` (`last_name`) " @@ -60,7 +60,7 @@ class Mysql2ActiveSchemaTest < ActiveRecord::Mysql2TestCase end def test_index_in_create - def (ActiveRecord::Base.connection).table_exists?(*); false; end + def (ActiveRecord::Base.connection).data_source_exists?(*); false; end %w(SPATIAL FULLTEXT UNIQUE).each do |type| expected = "CREATE TABLE `people` (#{type} INDEX `index_people_on_last_name` (`last_name`) ) ENGINE=InnoDB" @@ -78,7 +78,7 @@ class Mysql2ActiveSchemaTest < ActiveRecord::Mysql2TestCase end def test_index_in_bulk_change - def (ActiveRecord::Base.connection).table_exists?(*); true; end + def (ActiveRecord::Base.connection).data_source_exists?(*); true; end def (ActiveRecord::Base.connection).index_name_exists?(*); false; end %w(SPATIAL FULLTEXT UNIQUE).each do |type| @@ -152,7 +152,7 @@ class Mysql2ActiveSchemaTest < ActiveRecord::Mysql2TestCase end def test_indexes_in_create - ActiveRecord::Base.connection.stubs(:table_exists?).with(:temp).returns(false) + ActiveRecord::Base.connection.stubs(:data_source_exists?).with(:temp).returns(false) ActiveRecord::Base.connection.stubs(:index_name_exists?).with(:index_temp_on_zip).returns(false) expected = "CREATE TEMPORARY TABLE `temp` ( INDEX `index_temp_on_zip` (`zip`) ) ENGINE=InnoDB AS SELECT id, name, zip FROM a_really_complicated_query" diff --git a/activerecord/test/cases/adapters/mysql2/connection_test.rb b/activerecord/test/cases/adapters/mysql2/connection_test.rb index 71c4028675..507d024bb6 100644 --- a/activerecord/test/cases/adapters/mysql2/connection_test.rb +++ b/activerecord/test/cases/adapters/mysql2/connection_test.rb @@ -133,30 +133,30 @@ class Mysql2ConnectionTest < ActiveRecord::Mysql2TestCase end def test_get_and_release_advisory_lock - key = "test_key" + lock_name = "test_lock_name" - got_lock = @connection.get_advisory_lock(key) + got_lock = @connection.get_advisory_lock(lock_name) assert got_lock, "get_advisory_lock should have returned true but it didn't" - assert_equal test_lock_free(key), false, + assert_equal test_lock_free(lock_name), false, "expected the test advisory lock to be held but it wasn't" - released_lock = @connection.release_advisory_lock(key) + released_lock = @connection.release_advisory_lock(lock_name) assert released_lock, "expected release_advisory_lock to return true but it didn't" - assert test_lock_free(key), 'expected the test key to be available after releasing' + assert test_lock_free(lock_name), 'expected the test lock to be available after releasing' end def test_release_non_existent_advisory_lock - fake_key = "fake_key" - released_non_existent_lock = @connection.release_advisory_lock(fake_key) + lock_name = "fake_lock_name" + released_non_existent_lock = @connection.release_advisory_lock(lock_name) assert_equal released_non_existent_lock, false, 'expected release_advisory_lock to return false when there was no lock to release' end protected - def test_lock_free(key) - @connection.select_value("SELECT IS_FREE_LOCK('#{key}');") == 1 + def test_lock_free(lock_name) + @connection.select_value("SELECT IS_FREE_LOCK('#{lock_name}');") == 1 end end diff --git a/activerecord/test/cases/adapters/mysql2/schema_test.rb b/activerecord/test/cases/adapters/mysql2/schema_test.rb index 0d9f07917c..43957791b1 100644 --- a/activerecord/test/cases/adapters/mysql2/schema_test.rb +++ b/activerecord/test/cases/adapters/mysql2/schema_test.rb @@ -59,13 +59,13 @@ module ActiveRecord assert_equal 'id', @omgpost.primary_key end - def test_table_exists? + def test_data_source_exists? name = @omgpost.table_name - assert @connection.table_exists?(name), "#{name} table should exist" + assert @connection.data_source_exists?(name), "#{name} data_source should exist" end - def test_table_exists_wrong_schema - assert(!@connection.table_exists?("#{@db_name}.zomg"), "table should not exist") + def test_data_source_exists_wrong_schema + assert(!@connection.data_source_exists?("#{@db_name}.zomg"), "data_source should not exist") end def test_dump_indexes diff --git a/activerecord/test/cases/adapters/postgresql/connection_test.rb b/activerecord/test/cases/adapters/postgresql/connection_test.rb index b12beb91de..d559de3e28 100644 --- a/activerecord/test/cases/adapters/postgresql/connection_test.rb +++ b/activerecord/test/cases/adapters/postgresql/connection_test.rb @@ -90,7 +90,7 @@ module ActiveRecord end def test_tables_logs_name - @connection.tables('hello') + ActiveSupport::Deprecation.silence { @connection.tables('hello') } assert_equal 'SCHEMA', @subscriber.logged[0][1] end @@ -100,7 +100,7 @@ module ActiveRecord end def test_table_exists_logs_name - @connection.table_exists?('items') + ActiveSupport::Deprecation.silence { @connection.table_exists?('items') } assert_equal 'SCHEMA', @subscriber.logged[0][1] end @@ -211,33 +211,33 @@ module ActiveRecord end def test_get_and_release_advisory_lock - key = 5295901941911233559 + lock_id = 5295901941911233559 list_advisory_locks = <<-SQL SELECT locktype, - (classid::bigint << 32) | objid::bigint AS lock_key + (classid::bigint << 32) | objid::bigint AS lock_id FROM pg_locks WHERE locktype = 'advisory' SQL - got_lock = @connection.get_advisory_lock(key) + got_lock = @connection.get_advisory_lock(lock_id) assert got_lock, "get_advisory_lock should have returned true but it didn't" - advisory_lock = @connection.query(list_advisory_locks).find {|l| l[1] == key} + advisory_lock = @connection.query(list_advisory_locks).find {|l| l[1] == lock_id} assert advisory_lock, - "expected to find an advisory lock with key #{key} but there wasn't one" + "expected to find an advisory lock with lock_id #{lock_id} but there wasn't one" - released_lock = @connection.release_advisory_lock(key) + released_lock = @connection.release_advisory_lock(lock_id) assert released_lock, "expected release_advisory_lock to return true but it didn't" - advisory_locks = @connection.query(list_advisory_locks).select {|l| l[1] == key} + advisory_locks = @connection.query(list_advisory_locks).select {|l| l[1] == lock_id} assert_empty advisory_locks, - "expected to have released advisory lock with key #{key} but it was still held" + "expected to have released advisory lock with lock_id #{lock_id} but it was still held" end def test_release_non_existent_advisory_lock - fake_key = 2940075057017742022 + fake_lock_id = 2940075057017742022 with_warning_suppression do - released_non_existent_lock = @connection.release_advisory_lock(fake_key) + released_non_existent_lock = @connection.release_advisory_lock(fake_lock_id) assert_equal released_non_existent_lock, false, 'expected release_advisory_lock to return false when there was no lock to release' end diff --git a/activerecord/test/cases/adapters/postgresql/geometric_test.rb b/activerecord/test/cases/adapters/postgresql/geometric_test.rb index 0baf985654..3b97cb4ad4 100644 --- a/activerecord/test/cases/adapters/postgresql/geometric_test.rb +++ b/activerecord/test/cases/adapters/postgresql/geometric_test.rb @@ -167,16 +167,18 @@ class PostgresqlPointTest < ActiveRecord::PostgreSQLTestCase end class PostgresqlGeometricTest < ActiveRecord::PostgreSQLTestCase + include SchemaDumpingHelper + class PostgresqlGeometric < ActiveRecord::Base; end setup do @connection = ActiveRecord::Base.connection @connection.create_table("postgresql_geometrics") do |t| - t.column :a_line_segment, :lseg - t.column :a_box, :box - t.column :a_path, :path - t.column :a_polygon, :polygon - t.column :a_circle, :circle + t.lseg :a_line_segment + t.box :a_box + t.path :a_path + t.polygon :a_polygon + t.circle :a_circle end end @@ -233,4 +235,144 @@ class PostgresqlGeometricTest < ActiveRecord::PostgreSQLTestCase objs = PostgresqlGeometric.find_by_sql "SELECT isclosed(a_path) FROM postgresql_geometrics ORDER BY id ASC" assert_equal [false, true], objs.map(&:isclosed) end + + def test_schema_dumping + output = dump_table_schema("postgresql_geometrics") + assert_match %r{t\.lseg\s+"a_line_segment"$}, output + assert_match %r{t\.box\s+"a_box"$}, output + assert_match %r{t\.path\s+"a_path"$}, output + assert_match %r{t\.polygon\s+"a_polygon"$}, output + assert_match %r{t\.circle\s+"a_circle"$}, output + end +end + +class PostgreSQLGeometricLineTest < ActiveRecord::PostgreSQLTestCase + include SchemaDumpingHelper + + class PostgresqlLine < ActiveRecord::Base; end + + setup do + unless ActiveRecord::Base.connection.send(:postgresql_version) >= 90400 + skip("line type is not fully implemented") + end + @connection = ActiveRecord::Base.connection + @connection.create_table("postgresql_lines") do |t| + t.line :a_line + end + end + + teardown do + if defined?(@connection) + @connection.drop_table 'postgresql_lines', if_exists: true + end + end + + def test_geometric_line_type + g = PostgresqlLine.new( + a_line: '{2.0, 3, 5.5}' + ) + g.save! + + h = PostgresqlLine.find(g.id) + assert_equal '{2,3,5.5}', h.a_line + end + + def test_alternative_format_line_type + g = PostgresqlLine.new( + a_line: '(2.0, 3), (4.0, 6.0)' + ) + g.save! + + h = PostgresqlLine.find(g.id) + assert_equal '{1.5,-1,0}', h.a_line + end + + def test_schema_dumping_for_line_type + output = dump_table_schema("postgresql_lines") + assert_match %r{t\.line\s+"a_line"$}, output + end +end + +class PostgreSQLGeometricTypesTest < ActiveRecord::PostgreSQLTestCase + attr_reader :connection, :table_name + + def setup + super + @connection = ActiveRecord::Base.connection + @table_name = :testings + end + + def test_creating_column_with_point_type + connection.create_table(table_name) do |t| + t.point :foo_point + end + + assert_column_exists(:foo_point) + assert_type_correct(:foo_point, :point) + end + + def test_creating_column_with_line_type + connection.create_table(table_name) do |t| + t.line :foo_line + end + + assert_column_exists(:foo_line) + assert_type_correct(:foo_line, :line) + end + + def test_creating_column_with_lseg_type + connection.create_table(table_name) do |t| + t.lseg :foo_lseg + end + + assert_column_exists(:foo_lseg) + assert_type_correct(:foo_lseg, :lseg) + end + + def test_creating_column_with_box_type + connection.create_table(table_name) do |t| + t.box :foo_box + end + + assert_column_exists(:foo_box) + assert_type_correct(:foo_box, :box) + end + + def test_creating_column_with_path_type + connection.create_table(table_name) do |t| + t.path :foo_path + end + + assert_column_exists(:foo_path) + assert_type_correct(:foo_path, :path) + end + + def test_creating_column_with_polygon_type + connection.create_table(table_name) do |t| + t.polygon :foo_polygon + end + + assert_column_exists(:foo_polygon) + assert_type_correct(:foo_polygon, :polygon) + end + + def test_creating_column_with_circle_type + connection.create_table(table_name) do |t| + t.circle :foo_circle + end + + assert_column_exists(:foo_circle) + assert_type_correct(:foo_circle, :circle) + end + + private + + def assert_column_exists(column_name) + assert connection.column_exists?(table_name, column_name) + end + + def assert_type_correct(column_name, type) + column = connection.columns(table_name).find { |c| c.name == column_name.to_s } + assert_equal type, column.type + end end diff --git a/activerecord/test/cases/adapters/postgresql/schema_test.rb b/activerecord/test/cases/adapters/postgresql/schema_test.rb index 93e98ec872..7c9169f6e2 100644 --- a/activerecord/test/cases/adapters/postgresql/schema_test.rb +++ b/activerecord/test/cases/adapters/postgresql/schema_test.rb @@ -184,42 +184,42 @@ class SchemaTest < ActiveRecord::PostgreSQLTestCase @connection.exec_query("alter table developers drop column zomg", 'sql', []) if altered end - def test_table_exists? + def test_data_source_exists? [Thing1, Thing2, Thing3, Thing4].each do |klass| name = klass.table_name - assert @connection.table_exists?(name), "'#{name}' table should exist" + assert @connection.data_source_exists?(name), "'#{name}' data_source should exist" end end - def test_table_exists_when_on_schema_search_path + def test_data_source_exists_when_on_schema_search_path with_schema_search_path(SCHEMA_NAME) do - assert(@connection.table_exists?(TABLE_NAME), "table should exist and be found") + assert(@connection.data_source_exists?(TABLE_NAME), "data_source should exist and be found") end end - def test_table_exists_when_not_on_schema_search_path + def test_data_source_exists_when_not_on_schema_search_path with_schema_search_path('PUBLIC') do - assert(!@connection.table_exists?(TABLE_NAME), "table exists but should not be found") + assert(!@connection.data_source_exists?(TABLE_NAME), "data_source exists but should not be found") end end - def test_table_exists_wrong_schema - assert(!@connection.table_exists?("foo.things"), "table should not exist") + def test_data_source_exists_wrong_schema + assert(!@connection.data_source_exists?("foo.things"), "data_source should not exist") end - def test_table_exists_quoted_names + def test_data_source_exists_quoted_names [ %("#{SCHEMA_NAME}"."#{TABLE_NAME}"), %(#{SCHEMA_NAME}."#{TABLE_NAME}"), %(#{SCHEMA_NAME}."#{TABLE_NAME}")].each do |given| - assert(@connection.table_exists?(given), "table should exist when specified as #{given}") + assert(@connection.data_source_exists?(given), "data_source should exist when specified as #{given}") end with_schema_search_path(SCHEMA_NAME) do given = %("#{TABLE_NAME}") - assert(@connection.table_exists?(given), "table should exist when specified as #{given}") + assert(@connection.data_source_exists?(given), "data_source should exist when specified as #{given}") end end - def test_table_exists_quoted_table + def test_data_source_exists_quoted_table with_schema_search_path(SCHEMA_NAME) do - assert(@connection.table_exists?('"things.table"'), "table should exist") + assert(@connection.data_source_exists?('"things.table"'), "data_source should exist") end end diff --git a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb index 640df31e2e..a2fd1177a6 100644 --- a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb @@ -284,9 +284,9 @@ module ActiveRecord def test_tables with_example_table do - assert_equal %w{ ex }, @conn.tables + ActiveSupport::Deprecation.silence { assert_equal %w{ ex }, @conn.tables } with_example_table 'id integer PRIMARY KEY AUTOINCREMENT, number integer', 'people' do - assert_equal %w{ ex people }.sort, @conn.tables.sort + ActiveSupport::Deprecation.silence { assert_equal %w{ ex people }.sort, @conn.tables.sort } end end end @@ -297,7 +297,9 @@ module ActiveRecord WHERE type IN ('table','view') AND name <> 'sqlite_sequence' SQL assert_logged [[sql.squish, 'SCHEMA', []]] do - @conn.tables('hello') + ActiveSupport::Deprecation.silence do + @conn.tables('hello') + end end end @@ -316,7 +318,9 @@ module ActiveRecord WHERE type IN ('table','view') AND name <> 'sqlite_sequence' AND name = 'ex' SQL assert_logged [[sql.squish, 'SCHEMA', []]] do - assert @conn.table_exists?('ex') + ActiveSupport::Deprecation.silence do + assert @conn.table_exists?('ex') + end end end end diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index eb94870a35..ad157582a4 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -203,9 +203,22 @@ class HasManyAssociationsTest < ActiveRecord::TestCase bulb = car.bulbs.create assert_equal 'defaulty', bulb.name + end + + def test_build_and_create_from_association_should_respect_passed_attributes_over_default_scope + car = Car.create(name: 'honda') - bulb = car.bulbs.create(:name => 'exotic') + bulb = car.bulbs.build(name: 'exotic') assert_equal 'exotic', bulb.name + + bulb = car.bulbs.create(name: 'exotic') + assert_equal 'exotic', bulb.name + + bulb = car.awesome_bulbs.build(frickinawesome: false) + assert_equal false, bulb.frickinawesome + + bulb = car.awesome_bulbs.create(frickinawesome: false) + assert_equal false, bulb.frickinawesome end def test_build_from_association_should_respect_scope @@ -2335,6 +2348,12 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal [first_bulb, second_bulb], car.bulbs end + test 'double insertion of new object to association when same association used in the after create callback of a new object' do + car = Car.create! + car.bulbs << TrickyBulb.new + assert_equal 1, car.bulbs.size + end + def test_association_force_reload_with_only_true_is_deprecated company = Company.find(1) diff --git a/activerecord/test/cases/attributes_test.rb b/activerecord/test/cases/attributes_test.rb index 264b275181..2991ca8b76 100644 --- a/activerecord/test/cases/attributes_test.rb +++ b/activerecord/test/cases/attributes_test.rb @@ -172,5 +172,18 @@ module ActiveRecord assert_equal int_range, klass.type_for_attribute("my_int_range") end end + + test "attributes added after subclasses load are inherited" do + parent = Class.new(ActiveRecord::Base) do + self.table_name = "topics" + end + + child = Class.new(parent) + child.new # => force a schema load + + parent.attribute(:foo, Type::Value.new) + + assert_equal(:bar, child.new(foo: :bar).foo) + end end end diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index d961f4710e..3a9d60a79f 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -1345,6 +1345,19 @@ class BasicsTest < ActiveRecord::TestCase Company.attribute_names end + def test_has_attribute + assert Company.has_attribute?('id') + assert Company.has_attribute?('type') + assert Company.has_attribute?('name') + assert_not Company.has_attribute?('lastname') + assert_not Company.has_attribute?('age') + end + + def test_has_attribute_with_symbol + assert Company.has_attribute?(:id) + assert_not Company.has_attribute?(:age) + end + def test_attribute_names_on_table_not_exists assert_equal [], NonExistentTable.attribute_names end diff --git a/activerecord/test/cases/connection_management_test.rb b/activerecord/test/cases/connection_management_test.rb index cf8c4c688a..d43668e57c 100644 --- a/activerecord/test/cases/connection_management_test.rb +++ b/activerecord/test/cases/connection_management_test.rb @@ -92,14 +92,14 @@ module ActiveRecord app = lambda { |_| [200, {}, body] } response_body = ConnectionManagement.new(app).call(@env)[2] assert response_body.respond_to?(:to_path) - assert_equal response_body.to_path, "/path" + assert_equal "/path", response_body.to_path end test "doesn't mutate the original response" do original_response = [200, {}, 'hi'] app = lambda { |_| original_response } - response_body = ConnectionManagement.new(app).call(@env)[2] - assert_equal original_response.last, 'hi' + ConnectionManagement.new(app).call(@env)[2] + assert_equal 'hi', original_response.last end end end diff --git a/activerecord/test/cases/date_time_precision_test.rb b/activerecord/test/cases/date_time_precision_test.rb index 698f1b852e..e996d142a2 100644 --- a/activerecord/test/cases/date_time_precision_test.rb +++ b/activerecord/test/cases/date_time_precision_test.rb @@ -10,6 +10,7 @@ class DateTimePrecisionTest < ActiveRecord::TestCase setup do @connection = ActiveRecord::Base.connection + Foo.reset_column_information end teardown do @@ -20,24 +21,24 @@ class DateTimePrecisionTest < ActiveRecord::TestCase @connection.create_table(:foos, force: true) @connection.add_column :foos, :created_at, :datetime, precision: 0 @connection.add_column :foos, :updated_at, :datetime, precision: 5 - assert_equal 0, activerecord_column_option('foos', 'created_at', 'precision') - assert_equal 5, activerecord_column_option('foos', 'updated_at', 'precision') + assert_equal 0, Foo.columns_hash['created_at'].precision + assert_equal 5, Foo.columns_hash['updated_at'].precision end def test_timestamps_helper_with_custom_precision @connection.create_table(:foos, force: true) do |t| t.timestamps precision: 4 end - assert_equal 4, activerecord_column_option('foos', 'created_at', 'precision') - assert_equal 4, activerecord_column_option('foos', 'updated_at', 'precision') + assert_equal 4, Foo.columns_hash['created_at'].precision + assert_equal 4, Foo.columns_hash['updated_at'].precision end def test_passing_precision_to_datetime_does_not_set_limit @connection.create_table(:foos, force: true) do |t| t.timestamps precision: 4 end - assert_nil activerecord_column_option('foos', 'created_at', 'limit') - assert_nil activerecord_column_option('foos', 'updated_at', 'limit') + assert_nil Foo.columns_hash['created_at'].limit + assert_nil Foo.columns_hash['updated_at'].limit end def test_invalid_datetime_precision_raises_error @@ -48,14 +49,6 @@ class DateTimePrecisionTest < ActiveRecord::TestCase end end - def test_database_agrees_with_activerecord_about_precision - @connection.create_table(:foos, force: true) do |t| - t.timestamps precision: 4 - end - assert_equal 4, database_datetime_precision('foos', 'created_at') - assert_equal 4, database_datetime_precision('foos', 'updated_at') - end - def test_formatting_datetime_according_to_precision @connection.create_table(:foos, force: true) do |t| t.datetime :created_at, precision: 0 @@ -91,21 +84,5 @@ class DateTimePrecisionTest < ActiveRecord::TestCase end end - private - - def database_datetime_precision(table_name, column_name) - results = @connection.exec_query("SELECT column_name, datetime_precision FROM information_schema.columns WHERE table_name = '#{table_name}'") - result = results.find do |result_hash| - result_hash["column_name"] == column_name - end - result && result["datetime_precision"].to_i - end - - def activerecord_column_option(tablename, column_name, option) - result = @connection.columns(tablename).find do |column| - column.name == column_name - end - result && result.send(option) - end end end diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb index 6686ce012d..91214da048 100644 --- a/activerecord/test/cases/finder_test.rb +++ b/activerecord/test/cases/finder_test.rb @@ -706,96 +706,13 @@ class FinderTest < ActiveRecord::TestCase assert Company.where(["name = :name", {name: "37signals' go'es agains"}]).first end - def test_bind_arity - assert_nothing_raised { bind '' } - assert_raise(ActiveRecord::PreparedStatementInvalid) { bind '', 1 } - - assert_raise(ActiveRecord::PreparedStatementInvalid) { bind '?' } - assert_nothing_raised { bind '?', 1 } - assert_raise(ActiveRecord::PreparedStatementInvalid) { bind '?', 1, 1 } - end - def test_named_bind_variables - assert_equal '1', bind(':a', :a => 1) # ' ruby-mode - assert_equal '1 1', bind(':a :a', :a => 1) # ' ruby-mode - - assert_nothing_raised { bind("'+00:00'", :foo => "bar") } - assert_kind_of Firm, Company.where(["name = :name", { name: "37signals" }]).first assert_nil Company.where(["name = :name", { name: "37signals!" }]).first assert_nil Company.where(["name = :name", { name: "37signals!' OR 1=1" }]).first assert_kind_of Time, Topic.where(["id = :id", { id: 1 }]).first.written_on end - def test_named_bind_arity - assert_nothing_raised { bind "name = :name", { name: "37signals" } } - assert_nothing_raised { bind "name = :name", { name: "37signals", id: 1 } } - assert_raise(ActiveRecord::PreparedStatementInvalid) { bind "name = :name", { id: 1 } } - end - - class SimpleEnumerable - include Enumerable - - def initialize(ary) - @ary = ary - end - - def each(&b) - @ary.each(&b) - end - end - - def test_bind_enumerable - quoted_abc = %(#{ActiveRecord::Base.connection.quote('a')},#{ActiveRecord::Base.connection.quote('b')},#{ActiveRecord::Base.connection.quote('c')}) - - assert_equal '1,2,3', bind('?', [1, 2, 3]) - assert_equal quoted_abc, bind('?', %w(a b c)) - - assert_equal '1,2,3', bind(':a', :a => [1, 2, 3]) - assert_equal quoted_abc, bind(':a', :a => %w(a b c)) # ' - - assert_equal '1,2,3', bind('?', SimpleEnumerable.new([1, 2, 3])) - assert_equal quoted_abc, bind('?', SimpleEnumerable.new(%w(a b c))) - - assert_equal '1,2,3', bind(':a', :a => SimpleEnumerable.new([1, 2, 3])) - assert_equal quoted_abc, bind(':a', :a => SimpleEnumerable.new(%w(a b c))) # ' - end - - def test_bind_empty_enumerable - quoted_nil = ActiveRecord::Base.connection.quote(nil) - assert_equal quoted_nil, bind('?', []) - assert_equal " in (#{quoted_nil})", bind(' in (?)', []) - assert_equal "foo in (#{quoted_nil})", bind('foo in (?)', []) - end - - def test_bind_empty_string - quoted_empty = ActiveRecord::Base.connection.quote('') - assert_equal quoted_empty, bind('?', '') - end - - def test_bind_chars - quoted_bambi = ActiveRecord::Base.connection.quote("Bambi") - quoted_bambi_and_thumper = ActiveRecord::Base.connection.quote("Bambi\nand\nThumper") - assert_equal "name=#{quoted_bambi}", bind('name=?', "Bambi") - assert_equal "name=#{quoted_bambi_and_thumper}", bind('name=?', "Bambi\nand\nThumper") - assert_equal "name=#{quoted_bambi}", bind('name=?', "Bambi".mb_chars) - assert_equal "name=#{quoted_bambi_and_thumper}", bind('name=?', "Bambi\nand\nThumper".mb_chars) - end - - def test_bind_record - o = Struct.new(:quoted_id).new(1) - assert_equal '1', bind('?', o) - - os = [o] * 3 - assert_equal '1,1,1', bind('?', os) - end - - def test_named_bind_with_postgresql_type_casts - l = Proc.new { bind(":a::integer '2009-01-01'::date", :a => '10') } - assert_nothing_raised(&l) - assert_equal "#{ActiveRecord::Base.connection.quote('10')}::integer '2009-01-01'::date", l.call - end - def test_string_sanitation assert_not_equal "'something ' 1=1'", ActiveRecord::Base.sanitize("something ' 1=1") assert_equal "'something; select table'", ActiveRecord::Base.sanitize("something; select table") @@ -1136,14 +1053,6 @@ class FinderTest < ActiveRecord::TestCase end protected - def bind(statement, *vars) - if vars.first.is_a?(Hash) - ActiveRecord::Base.send(:replace_named_bind_variables, statement, vars.first) - else - ActiveRecord::Base.send(:replace_bind_variables, statement, vars) - end - end - def table_with_custom_primary_key yield(Class.new(Toy) do def self.name diff --git a/activerecord/test/cases/forbidden_attributes_protection_test.rb b/activerecord/test/cases/forbidden_attributes_protection_test.rb index f4e7646f03..91921469b8 100644 --- a/activerecord/test/cases/forbidden_attributes_protection_test.rb +++ b/activerecord/test/cases/forbidden_attributes_protection_test.rb @@ -1,14 +1,20 @@ require 'cases/helper' require 'active_support/core_ext/hash/indifferent_access' -require 'models/person' + require 'models/company' +require 'models/person' +require 'models/ship' +require 'models/ship_part' +require 'models/treasure' -class ProtectedParams < ActiveSupport::HashWithIndifferentAccess +class ProtectedParams attr_accessor :permitted alias :permitted? :permitted + delegate :keys, :key?, :has_key?, :empty?, to: :@parameters + def initialize(attributes) - super(attributes) + @parameters = attributes.with_indifferent_access @permitted = false end @@ -17,6 +23,18 @@ class ProtectedParams < ActiveSupport::HashWithIndifferentAccess self end + def [](key) + @parameters[key] + end + + def to_h + @parameters + end + + def stringify_keys + dup + end + def dup super.tap do |duplicate| duplicate.instance_variable_set :@permitted, @permitted @@ -75,6 +93,13 @@ class ForbiddenAttributesProtectionTest < ActiveRecord::TestCase end end + def test_create_with_works_with_permitted_params + params = ProtectedParams.new(first_name: 'Guille').permit! + + person = Person.create_with(params).create! + assert_equal 'Guille', person.first_name + end + def test_create_with_works_with_params_values params = ProtectedParams.new(first_name: 'Guille') @@ -90,10 +115,51 @@ class ForbiddenAttributesProtectionTest < ActiveRecord::TestCase end end + def test_where_works_with_permitted_params + params = ProtectedParams.new(first_name: 'Guille').permit! + + person = Person.where(params).create! + assert_equal 'Guille', person.first_name + end + def test_where_works_with_params_values params = ProtectedParams.new(first_name: 'Guille') person = Person.where(first_name: params[:first_name]).create! assert_equal 'Guille', person.first_name end + + def test_where_not_checks_permitted + params = ProtectedParams.new(first_name: 'Guille', gender: 'm') + + assert_raises(ActiveModel::ForbiddenAttributesError) do + Person.where().not(params) + end + end + + def test_where_not_works_with_permitted_params + params = ProtectedParams.new(first_name: 'Guille').permit! + Person.create!(params) + assert_empty Person.where.not(params).select {|p| p.first_name == 'Guille' } + end + + def test_strong_params_style_objects_work_with_singular_associations + params = ProtectedParams.new( name: "Stern", ship_attributes: ProtectedParams.new(name: "The Black Rock").permit!).permit! + part = ShipPart.new(params) + + assert_equal "Stern", part.name + assert_equal "The Black Rock", part.ship.name + end + + def test_strong_params_style_objects_work_with_collection_associations + params = ProtectedParams.new( + trinkets_attributes: ProtectedParams.new( + "0" => ProtectedParams.new(name: "Necklace").permit!, + "1" => ProtectedParams.new(name: "Spoon").permit! ) ).permit! + part = ShipPart.new(params) + + assert_equal "Necklace", part.trinkets[0].name + assert_equal "Spoon", part.trinkets[1].name + end + end diff --git a/activerecord/test/cases/helper.rb b/activerecord/test/cases/helper.rb index d82a3040fc..07dbc8a53f 100644 --- a/activerecord/test/cases/helper.rb +++ b/activerecord/test/cases/helper.rb @@ -47,7 +47,7 @@ def in_memory_db? end def subsecond_precision_supported? - !current_adapter?(:MysqlAdapter, :Mysql2Adapter) || ActiveRecord::Base.connection.version >= '5.6.4' + ActiveRecord::Base.connection.supports_datetime_with_precision? end def mysql_enforcing_gtid_consistency? diff --git a/activerecord/test/cases/inheritance_test.rb b/activerecord/test/cases/inheritance_test.rb index 52e3734dd0..03bce547da 100644 --- a/activerecord/test/cases/inheritance_test.rb +++ b/activerecord/test/cases/inheritance_test.rb @@ -478,4 +478,49 @@ class InheritanceComputeTypeTest < ActiveRecord::TestCase product = Shop::Product.new(:type => phone) assert product.save end + + def test_inheritance_new_with_subclass_as_default + original_type = Company.columns_hash["type"].default + ActiveRecord::Base.connection.change_column_default :companies, :type, 'Firm' + Company.reset_column_information + + firm = Company.new # without arguments + assert_equal 'Firm', firm.type + assert_instance_of Firm, firm + + firm = Company.new(firm_name: 'Shri Hans Plastic') # with arguments + assert_equal 'Firm', firm.type + assert_instance_of Firm, firm + + firm = Company.new(type: 'Client') # overwrite the default type + assert_equal 'Client', firm.type + assert_instance_of Client, firm + ensure + ActiveRecord::Base.connection.change_column_default :companies, :type, original_type + Company.reset_column_information + end +end + +class InheritanceAttributeTest < ActiveRecord::TestCase + + class Company < ActiveRecord::Base + self.table_name = 'companies' + attribute :type, :string, default: "InheritanceAttributeTest::Startup" + end + + class Startup < Company + end + + class Empire < Company + end + + def test_inheritance_new_with_subclass_as_default + startup = Company.new # without arguments + assert_equal 'InheritanceAttributeTest::Startup', startup.type + assert_instance_of Startup, startup + + empire = Company.new(type: 'InheritanceAttributeTest::Empire') # without arguments + assert_equal 'InheritanceAttributeTest::Empire', empire.type + assert_instance_of Empire, empire + end end diff --git a/activerecord/test/cases/invertible_migration_test.rb b/activerecord/test/cases/invertible_migration_test.rb index 84b0ff8fcb..0da58040c8 100644 --- a/activerecord/test/cases/invertible_migration_test.rb +++ b/activerecord/test/cases/invertible_migration_test.rb @@ -157,8 +157,10 @@ module ActiveRecord teardown do %w[horses new_horses].each do |table| - if ActiveRecord::Base.connection.table_exists?(table) - ActiveRecord::Base.connection.drop_table(table) + ActiveSupport::Deprecation.silence do + if ActiveRecord::Base.connection.table_exists?(table) + ActiveRecord::Base.connection.drop_table(table) + end end end ActiveRecord::Migration.verbose = @verbose_was @@ -189,14 +191,14 @@ module ActiveRecord def test_migrate_up migration = InvertibleMigration.new migration.migrate(:up) - assert migration.connection.table_exists?("horses"), "horses should exist" + ActiveSupport::Deprecation.silence { assert migration.connection.table_exists?("horses"), "horses should exist" } end def test_migrate_down migration = InvertibleMigration.new migration.migrate :up migration.migrate :down - assert !migration.connection.table_exists?("horses") + ActiveSupport::Deprecation.silence { assert !migration.connection.table_exists?("horses") } end def test_migrate_revert @@ -204,11 +206,11 @@ module ActiveRecord revert = InvertibleRevertMigration.new migration.migrate :up revert.migrate :up - assert !migration.connection.table_exists?("horses") + ActiveSupport::Deprecation.silence { assert !migration.connection.table_exists?("horses") } revert.migrate :down - assert migration.connection.table_exists?("horses") + ActiveSupport::Deprecation.silence { assert migration.connection.table_exists?("horses") } migration.migrate :down - assert !migration.connection.table_exists?("horses") + ActiveSupport::Deprecation.silence { assert !migration.connection.table_exists?("horses") } end def test_migrate_revert_by_part @@ -216,18 +218,24 @@ module ActiveRecord received = [] migration = InvertibleByPartsMigration.new migration.test = ->(dir){ - assert migration.connection.table_exists?("horses") - assert migration.connection.table_exists?("new_horses") + ActiveSupport::Deprecation.silence do + assert migration.connection.table_exists?("horses") + assert migration.connection.table_exists?("new_horses") + end received << dir } migration.migrate :up assert_equal [:both, :up], received - assert !migration.connection.table_exists?("horses") - assert migration.connection.table_exists?("new_horses") + ActiveSupport::Deprecation.silence do + assert !migration.connection.table_exists?("horses") + assert migration.connection.table_exists?("new_horses") + end migration.migrate :down assert_equal [:both, :up, :both, :down], received - assert migration.connection.table_exists?("horses") - assert !migration.connection.table_exists?("new_horses") + ActiveSupport::Deprecation.silence do + assert migration.connection.table_exists?("horses") + assert !migration.connection.table_exists?("new_horses") + end end def test_migrate_revert_whole_migration @@ -236,20 +244,20 @@ module ActiveRecord revert = RevertWholeMigration.new(klass) migration.migrate :up revert.migrate :up - assert !migration.connection.table_exists?("horses") + ActiveSupport::Deprecation.silence { assert !migration.connection.table_exists?("horses") } revert.migrate :down - assert migration.connection.table_exists?("horses") + ActiveSupport::Deprecation.silence { assert migration.connection.table_exists?("horses") } migration.migrate :down - assert !migration.connection.table_exists?("horses") + ActiveSupport::Deprecation.silence { assert !migration.connection.table_exists?("horses") } end end def test_migrate_nested_revert_whole_migration revert = NestedRevertWholeMigration.new(InvertibleRevertMigration) revert.migrate :down - assert revert.connection.table_exists?("horses") + ActiveSupport::Deprecation.silence { assert revert.connection.table_exists?("horses") } revert.migrate :up - assert !revert.connection.table_exists?("horses") + ActiveSupport::Deprecation.silence { assert !revert.connection.table_exists?("horses") } end def test_migrate_revert_change_column_default @@ -314,24 +322,24 @@ module ActiveRecord def test_legacy_up LegacyMigration.migrate :up - assert ActiveRecord::Base.connection.table_exists?("horses"), "horses should exist" + ActiveSupport::Deprecation.silence { assert ActiveRecord::Base.connection.table_exists?("horses"), "horses should exist" } end def test_legacy_down LegacyMigration.migrate :up LegacyMigration.migrate :down - assert !ActiveRecord::Base.connection.table_exists?("horses"), "horses should not exist" + ActiveSupport::Deprecation.silence { assert !ActiveRecord::Base.connection.table_exists?("horses"), "horses should not exist" } end def test_up LegacyMigration.up - assert ActiveRecord::Base.connection.table_exists?("horses"), "horses should exist" + ActiveSupport::Deprecation.silence { assert ActiveRecord::Base.connection.table_exists?("horses"), "horses should exist" } end def test_down LegacyMigration.up LegacyMigration.down - assert !ActiveRecord::Base.connection.table_exists?("horses"), "horses should not exist" + ActiveSupport::Deprecation.silence { assert !ActiveRecord::Base.connection.table_exists?("horses"), "horses should not exist" } end def test_migrate_down_with_table_name_prefix @@ -340,7 +348,7 @@ module ActiveRecord migration = InvertibleMigration.new migration.migrate(:up) assert_nothing_raised { migration.migrate(:down) } - assert !ActiveRecord::Base.connection.table_exists?("p_horses_s"), "p_horses_s should not exist" + ActiveSupport::Deprecation.silence { assert !ActiveRecord::Base.connection.table_exists?("p_horses_s"), "p_horses_s should not exist" } ensure ActiveRecord::Base.table_name_prefix = ActiveRecord::Base.table_name_suffix = '' end diff --git a/activerecord/test/cases/migration/change_schema_test.rb b/activerecord/test/cases/migration/change_schema_test.rb index 83e50048ec..2ff9cf8cf5 100644 --- a/activerecord/test/cases/migration/change_schema_test.rb +++ b/activerecord/test/cases/migration/change_schema_test.rb @@ -405,9 +405,9 @@ module ActiveRecord def test_drop_table_if_exists connection.create_table(:testings) - assert connection.table_exists?(:testings) + ActiveSupport::Deprecation.silence { assert connection.table_exists?(:testings) } connection.drop_table(:testings, if_exists: true) - assert_not connection.table_exists?(:testings) + ActiveSupport::Deprecation.silence { assert_not connection.table_exists?(:testings) } end def test_drop_table_if_exists_nothing_raised diff --git a/activerecord/test/cases/migration/create_join_table_test.rb b/activerecord/test/cases/migration/create_join_table_test.rb index 8fd08fe4ce..0a7b57455c 100644 --- a/activerecord/test/cases/migration/create_join_table_test.rb +++ b/activerecord/test/cases/migration/create_join_table_test.rb @@ -12,7 +12,9 @@ module ActiveRecord teardown do %w(artists_musics musics_videos catalog).each do |table_name| - connection.drop_table table_name if connection.tables.include?(table_name) + ActiveSupport::Deprecation.silence do + connection.drop_table table_name if connection.table_exists?(table_name) + end end end @@ -82,62 +84,62 @@ module ActiveRecord connection.create_join_table :artists, :musics connection.drop_join_table :artists, :musics - assert !connection.tables.include?('artists_musics') + ActiveSupport::Deprecation.silence { assert !connection.table_exists?('artists_musics') } end def test_drop_join_table_with_strings connection.create_join_table :artists, :musics connection.drop_join_table 'artists', 'musics' - assert !connection.tables.include?('artists_musics') + ActiveSupport::Deprecation.silence { assert !connection.table_exists?('artists_musics') } end def test_drop_join_table_with_the_proper_order connection.create_join_table :videos, :musics connection.drop_join_table :videos, :musics - assert !connection.tables.include?('musics_videos') + ActiveSupport::Deprecation.silence { assert !connection.table_exists?('musics_videos') } end def test_drop_join_table_with_the_table_name connection.create_join_table :artists, :musics, table_name: :catalog connection.drop_join_table :artists, :musics, table_name: :catalog - assert !connection.tables.include?('catalog') + ActiveSupport::Deprecation.silence { assert !connection.table_exists?('catalog') } end def test_drop_join_table_with_the_table_name_as_string connection.create_join_table :artists, :musics, table_name: 'catalog' connection.drop_join_table :artists, :musics, table_name: 'catalog' - assert !connection.tables.include?('catalog') + ActiveSupport::Deprecation.silence { assert !connection.table_exists?('catalog') } end def test_drop_join_table_with_column_options connection.create_join_table :artists, :musics, column_options: {null: true} connection.drop_join_table :artists, :musics, column_options: {null: true} - assert !connection.tables.include?('artists_musics') + ActiveSupport::Deprecation.silence { assert !connection.table_exists?('artists_musics') } end def test_create_and_drop_join_table_with_common_prefix with_table_cleanup do connection.create_join_table 'audio_artists', 'audio_musics' - assert_includes connection.tables, 'audio_artists_musics' + ActiveSupport::Deprecation.silence { assert connection.table_exists?('audio_artists_musics') } connection.drop_join_table 'audio_artists', 'audio_musics' - assert !connection.tables.include?('audio_artists_musics'), "Should have dropped join table, but didn't" + ActiveSupport::Deprecation.silence { assert !connection.table_exists?('audio_artists_musics'), "Should have dropped join table, but didn't" } end end private def with_table_cleanup - tables_before = connection.tables + tables_before = connection.data_sources yield ensure - tables_after = connection.tables - tables_before + tables_after = connection.data_sources - tables_before tables_after.each do |table| connection.drop_table table diff --git a/activerecord/test/cases/migration/postgresql_geometric_types_test.rb b/activerecord/test/cases/migration/postgresql_geometric_types_test.rb deleted file mode 100644 index e4772905bb..0000000000 --- a/activerecord/test/cases/migration/postgresql_geometric_types_test.rb +++ /dev/null @@ -1,93 +0,0 @@ -require 'cases/helper' - -module ActiveRecord - class Migration - class PostgreSQLGeometricTypesTest < ActiveRecord::TestCase - attr_reader :connection, :table_name - - def setup - super - @connection = ActiveRecord::Base.connection - @table_name = :testings - end - - if current_adapter?(:PostgreSQLAdapter) - def test_creating_column_with_point_type - connection.create_table(table_name) do |t| - t.point :foo_point - end - - assert_column_exists(:foo_point) - assert_type_correct(:foo_point, :point) - end - - def test_creating_column_with_line_type - connection.create_table(table_name) do |t| - t.line :foo_line - end - - assert_column_exists(:foo_line) - assert_type_correct(:foo_line, :line) - end - - def test_creating_column_with_lseg_type - connection.create_table(table_name) do |t| - t.lseg :foo_lseg - end - - assert_column_exists(:foo_lseg) - assert_type_correct(:foo_lseg, :lseg) - end - - def test_creating_column_with_box_type - connection.create_table(table_name) do |t| - t.box :foo_box - end - - assert_column_exists(:foo_box) - assert_type_correct(:foo_box, :box) - end - - def test_creating_column_with_path_type - connection.create_table(table_name) do |t| - t.path :foo_path - end - - assert_column_exists(:foo_path) - assert_type_correct(:foo_path, :path) - end - - def test_creating_column_with_polygon_type - connection.create_table(table_name) do |t| - t.polygon :foo_polygon - end - - assert_column_exists(:foo_polygon) - assert_type_correct(:foo_polygon, :polygon) - end - - def test_creating_column_with_circle_type - connection.create_table(table_name) do |t| - t.circle :foo_circle - end - - assert_column_exists(:foo_circle) - assert_type_correct(:foo_circle, :circle) - end - end - - private - def assert_column_exists(column_name) - columns = connection.columns(table_name) - assert columns.map(&:name).include?(column_name.to_s) - end - - def assert_type_correct(column_name, type) - columns = connection.columns(table_name) - column = columns.select{ |c| c.name == column_name.to_s }.first - assert_equal type.to_s, column.sql_type - end - - end - end -end
\ No newline at end of file diff --git a/activerecord/test/cases/migration/references_foreign_key_test.rb b/activerecord/test/cases/migration/references_foreign_key_test.rb index 84ec657398..edbc8abe4d 100644 --- a/activerecord/test/cases/migration/references_foreign_key_test.rb +++ b/activerecord/test/cases/migration/references_foreign_key_test.rb @@ -164,7 +164,7 @@ class ReferencesWithoutForeignKeySupportTest < ActiveRecord::TestCase t.references :testing_parent, foreign_key: true end - assert_includes @connection.tables, "testings" + assert_includes @connection.data_sources, "testings" end end end diff --git a/activerecord/test/cases/migration/rename_table_test.rb b/activerecord/test/cases/migration/rename_table_test.rb index 6d742d3f2f..8eb027d53f 100644 --- a/activerecord/test/cases/migration/rename_table_test.rb +++ b/activerecord/test/cases/migration/rename_table_test.rb @@ -15,7 +15,7 @@ module ActiveRecord end def teardown - rename_table :octopi, :test_models if connection.table_exists? :octopi + ActiveSupport::Deprecation.silence { rename_table :octopi, :test_models if connection.table_exists? :octopi } super end @@ -83,7 +83,7 @@ module ActiveRecord enable_extension!('uuid-ossp', connection) connection.create_table :cats, id: :uuid assert_nothing_raised { rename_table :cats, :felines } - assert connection.table_exists? :felines + ActiveSupport::Deprecation.silence { assert connection.table_exists? :felines } ensure disable_extension!('uuid-ossp', connection) connection.drop_table :cats, if_exists: true diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb index 15a0e0516d..c3c204cf9f 100644 --- a/activerecord/test/cases/migration_test.rb +++ b/activerecord/test/cases/migration_test.rb @@ -524,33 +524,33 @@ class MigrationTest < ActiveRecord::TestCase end if ActiveRecord::Base.connection.supports_advisory_locks? - def test_migrator_generates_valid_lock_key + def test_migrator_generates_valid_lock_id migration = Class.new(ActiveRecord::Migration).new migrator = ActiveRecord::Migrator.new(:up, [migration], 100) - lock_key = migrator.send(:generate_migrator_advisory_lock_key) + lock_id = migrator.send(:generate_migrator_advisory_lock_id) - assert ActiveRecord::Base.connection.get_advisory_lock(lock_key), - "the Migrator should have generated a valid lock key, but it didn't" - assert ActiveRecord::Base.connection.release_advisory_lock(lock_key), - "the Migrator should have generated a valid lock key, but it didn't" + assert ActiveRecord::Base.connection.get_advisory_lock(lock_id), + "the Migrator should have generated a valid lock id, but it didn't" + assert ActiveRecord::Base.connection.release_advisory_lock(lock_id), + "the Migrator should have generated a valid lock id, but it didn't" end - def test_generate_migrator_advisory_lock_key + def test_generate_migrator_advisory_lock_id # It is important we are consistent with how we generate this so that # exclusive locking works across migrator versions migration = Class.new(ActiveRecord::Migration).new migrator = ActiveRecord::Migrator.new(:up, [migration], 100) - lock_key = migrator.send(:generate_migrator_advisory_lock_key) + lock_id = migrator.send(:generate_migrator_advisory_lock_id) current_database = ActiveRecord::Base.connection.current_database salt = ActiveRecord::Migrator::MIGRATOR_SALT - expected_key = Zlib.crc32(current_database) * salt + expected_id = Zlib.crc32(current_database) * salt - assert lock_key == expected_key, "expected lock key generated by the migrator to be #{expected_key}, but it was #{lock_key} instead" - assert lock_key.is_a?(Fixnum), "expected lock key to be a Fixnum, but it wasn't" - assert lock_key.bit_length <= 63, "lock key must be a signed integer of max 63 bits magnitude" + assert lock_id == expected_id, "expected lock id generated by the migrator to be #{expected_id}, but it was #{lock_id} instead" + assert lock_id.is_a?(Fixnum), "expected lock id to be a Fixnum, but it wasn't" + assert lock_id.bit_length <= 63, "lock id must be a signed integer of max 63 bits magnitude" end def test_migrator_one_up_with_unavailable_lock @@ -564,9 +564,9 @@ class MigrationTest < ActiveRecord::TestCase }.new migrator = ActiveRecord::Migrator.new(:up, [migration], 100) - lock_key = migrator.send(:generate_migrator_advisory_lock_key) + lock_id = migrator.send(:generate_migrator_advisory_lock_id) - with_another_process_holding_lock(lock_key) do + with_another_process_holding_lock(lock_id) do assert_raise(ActiveRecord::ConcurrentMigrationError) { migrator.migrate } end @@ -585,9 +585,9 @@ class MigrationTest < ActiveRecord::TestCase }.new migrator = ActiveRecord::Migrator.new(:up, [migration], 100) - lock_key = migrator.send(:generate_migrator_advisory_lock_key) + lock_id = migrator.send(:generate_migrator_advisory_lock_id) - with_another_process_holding_lock(lock_key) do + with_another_process_holding_lock(lock_id) do assert_raise(ActiveRecord::ConcurrentMigrationError) { migrator.run } end @@ -606,18 +606,18 @@ class MigrationTest < ActiveRecord::TestCase } end - def with_another_process_holding_lock(lock_key) + def with_another_process_holding_lock(lock_id) thread_lock = Concurrent::CountDownLatch.new test_terminated = Concurrent::CountDownLatch.new other_process = Thread.new do begin conn = ActiveRecord::Base.connection_pool.checkout - conn.get_advisory_lock(lock_key) + conn.get_advisory_lock(lock_id) thread_lock.count_down test_terminated.wait # hold the lock open until we tested everything ensure - conn.release_advisory_lock(lock_key) + conn.release_advisory_lock(lock_id) ActiveRecord::Base.connection_pool.checkin(conn) end end diff --git a/activerecord/test/cases/migrator_test.rb b/activerecord/test/cases/migrator_test.rb index 2ff6938e7b..dbf088f455 100644 --- a/activerecord/test/cases/migrator_test.rb +++ b/activerecord/test/cases/migrator_test.rb @@ -313,9 +313,9 @@ class MigratorTest < ActiveRecord::TestCase _, migrator = migrator_class(3) ActiveRecord::Base.connection.drop_table "schema_migrations", if_exists: true - assert_not ActiveRecord::Base.connection.table_exists?('schema_migrations') + ActiveSupport::Deprecation.silence { assert_not ActiveRecord::Base.connection.table_exists?('schema_migrations') } migrator.migrate("valid", 1) - assert ActiveRecord::Base.connection.table_exists?('schema_migrations') + ActiveSupport::Deprecation.silence { assert ActiveRecord::Base.connection.table_exists?('schema_migrations') } end def test_migrator_forward diff --git a/activerecord/test/cases/nested_attributes_test.rb b/activerecord/test/cases/nested_attributes_test.rb index 93cb631a04..0b700afcb4 100644 --- a/activerecord/test/cases/nested_attributes_test.rb +++ b/activerecord/test/cases/nested_attributes_test.rb @@ -1068,39 +1068,4 @@ class TestHasManyAutosaveAssociationWhichItselfHasAutosaveAssociations < ActiveR assert_not part.valid? assert_equal ["Ship name can't be blank"], part.errors.full_messages end - - class ProtectedParameters - def initialize(hash) - @hash = hash - end - - def permitted? - true - end - - def [](key) - @hash[key] - end - - def to_h - @hash - end - end - - test "strong params style objects can be assigned for singular associations" do - params = { name: "Stern", ship_attributes: - ProtectedParameters.new(name: "The Black Rock") } - part = ShipPart.new(params) - - assert_equal "Stern", part.name - assert_equal "The Black Rock", part.ship.name - end - - test "strong params style objects can be assigned for collection associations" do - params = { trinkets_attributes: ProtectedParameters.new("0" => ProtectedParameters.new(name: "Necklace"), "1" => ProtectedParameters.new(name: "Spoon")) } - part = ShipPart.new(params) - - assert_equal "Necklace", part.trinkets[0].name - assert_equal "Spoon", part.trinkets[1].name - end end diff --git a/activerecord/test/cases/persistence_test.rb b/activerecord/test/cases/persistence_test.rb index acc3103ac6..b2e59e3970 100644 --- a/activerecord/test/cases/persistence_test.rb +++ b/activerecord/test/cases/persistence_test.rb @@ -966,4 +966,17 @@ class PersistenceTest < ActiveRecord::TestCase widget.reset_column_information end end + + def test_reset_column_information_resets_children + child = Class.new(Topic) + child.new # force schema to load + + ActiveRecord::Base.connection.add_column(:topics, :foo, :string) + Topic.reset_column_information + + assert_equal "bar", child.new(foo: :bar).foo + ensure + ActiveRecord::Base.connection.remove_column(:topics, :foo) + Topic.reset_column_information + end end diff --git a/activerecord/test/cases/pooled_connections_test.rb b/activerecord/test/cases/pooled_connections_test.rb index daa3271777..bca50dd008 100644 --- a/activerecord/test/cases/pooled_connections_test.rb +++ b/activerecord/test/cases/pooled_connections_test.rb @@ -44,7 +44,7 @@ class PooledConnectionsTest < ActiveRecord::TestCase conn = ActiveRecord::Base.connection_pool.checkout ActiveRecord::Base.connection_pool.checkin conn @connection_count += 1 - ActiveRecord::Base.connection.tables + ActiveRecord::Base.connection.data_sources rescue ActiveRecord::ConnectionTimeoutError @timed_out += 1 end diff --git a/activerecord/test/cases/primary_keys_test.rb b/activerecord/test/cases/primary_keys_test.rb index 344822c883..d883784553 100644 --- a/activerecord/test/cases/primary_keys_test.rb +++ b/activerecord/test/cases/primary_keys_test.rb @@ -224,7 +224,7 @@ class PrimaryKeyAnyTypeTest < ActiveRecord::TestCase end teardown do - @connection.drop_table(:barcodes) if @connection.table_exists? :barcodes + @connection.drop_table(:barcodes, if_exists: true) end def test_any_type_primary_key diff --git a/activerecord/test/cases/relation/delegation_test.rb b/activerecord/test/cases/relation/delegation_test.rb index 989f4e1e5d..f0e07e0731 100644 --- a/activerecord/test/cases/relation/delegation_test.rb +++ b/activerecord/test/cases/relation/delegation_test.rb @@ -27,7 +27,7 @@ module ActiveRecord module DelegationWhitelistBlacklistTests ARRAY_DELEGATES = [ - :+, :-, :|, :&, :[], + :+, :-, :|, :&, :[], :shuffle, :all?, :collect, :compact, :detect, :each, :each_cons, :each_with_index, :exclude?, :find_all, :flat_map, :group_by, :include?, :length, :map, :none?, :one?, :partition, :reject, :reverse, @@ -40,12 +40,6 @@ module ActiveRecord assert_respond_to target, method end end - - ActiveRecord::Delegation::BLACKLISTED_ARRAY_METHODS.each do |method| - define_method "test_#{method}_is_not_delegated_to_Array" do - assert_raises(NoMethodError) { call_method(target, method) } - end - end end class DelegationAssociationTest < DelegationTest diff --git a/activerecord/test/cases/relation_test.rb b/activerecord/test/cases/relation_test.rb index 8794bc8043..f46d414b95 100644 --- a/activerecord/test/cases/relation_test.rb +++ b/activerecord/test/cases/relation_test.rb @@ -235,6 +235,13 @@ module ActiveRecord assert_equal 3, relation.where(id: post.id).pluck(:id).size end + def test_merge_raises_with_invalid_argument + assert_raises ArgumentError do + relation = Relation.new(FakeKlass, :b, nil) + relation.merge(true) + end + end + def test_respond_to_for_non_selected_element post = Post.select(:title).first assert_equal false, post.respond_to?(:body), "post should not respond_to?(:body) since invoking it raises exception" diff --git a/activerecord/test/cases/sanitize_test.rb b/activerecord/test/cases/sanitize_test.rb index aa5c04dd06..239f63d27b 100644 --- a/activerecord/test/cases/sanitize_test.rb +++ b/activerecord/test/cases/sanitize_test.rb @@ -79,4 +79,98 @@ class SanitizeTest < ActiveRecord::TestCase searchable_post.search("20% _reduction_!").to_a end end + + def test_bind_arity + assert_nothing_raised { bind '' } + assert_raise(ActiveRecord::PreparedStatementInvalid) { bind '', 1 } + + assert_raise(ActiveRecord::PreparedStatementInvalid) { bind '?' } + assert_nothing_raised { bind '?', 1 } + assert_raise(ActiveRecord::PreparedStatementInvalid) { bind '?', 1, 1 } + end + + def test_named_bind_variables + assert_equal '1', bind(':a', :a => 1) # ' ruby-mode + assert_equal '1 1', bind(':a :a', :a => 1) # ' ruby-mode + + assert_nothing_raised { bind("'+00:00'", :foo => "bar") } + end + + def test_named_bind_arity + assert_nothing_raised { bind "name = :name", { name: "37signals" } } + assert_nothing_raised { bind "name = :name", { name: "37signals", id: 1 } } + assert_raise(ActiveRecord::PreparedStatementInvalid) { bind "name = :name", { id: 1 } } + end + + class SimpleEnumerable + include Enumerable + + def initialize(ary) + @ary = ary + end + + def each(&b) + @ary.each(&b) + end + end + + def test_bind_enumerable + quoted_abc = %(#{ActiveRecord::Base.connection.quote('a')},#{ActiveRecord::Base.connection.quote('b')},#{ActiveRecord::Base.connection.quote('c')}) + + assert_equal '1,2,3', bind('?', [1, 2, 3]) + assert_equal quoted_abc, bind('?', %w(a b c)) + + assert_equal '1,2,3', bind(':a', :a => [1, 2, 3]) + assert_equal quoted_abc, bind(':a', :a => %w(a b c)) # ' + + assert_equal '1,2,3', bind('?', SimpleEnumerable.new([1, 2, 3])) + assert_equal quoted_abc, bind('?', SimpleEnumerable.new(%w(a b c))) + + assert_equal '1,2,3', bind(':a', :a => SimpleEnumerable.new([1, 2, 3])) + assert_equal quoted_abc, bind(':a', :a => SimpleEnumerable.new(%w(a b c))) # ' + end + + def test_bind_empty_enumerable + quoted_nil = ActiveRecord::Base.connection.quote(nil) + assert_equal quoted_nil, bind('?', []) + assert_equal " in (#{quoted_nil})", bind(' in (?)', []) + assert_equal "foo in (#{quoted_nil})", bind('foo in (?)', []) + end + + def test_bind_empty_string + quoted_empty = ActiveRecord::Base.connection.quote('') + assert_equal quoted_empty, bind('?', '') + end + + def test_bind_chars + quoted_bambi = ActiveRecord::Base.connection.quote("Bambi") + quoted_bambi_and_thumper = ActiveRecord::Base.connection.quote("Bambi\nand\nThumper") + assert_equal "name=#{quoted_bambi}", bind('name=?', "Bambi") + assert_equal "name=#{quoted_bambi_and_thumper}", bind('name=?', "Bambi\nand\nThumper") + assert_equal "name=#{quoted_bambi}", bind('name=?', "Bambi".mb_chars) + assert_equal "name=#{quoted_bambi_and_thumper}", bind('name=?', "Bambi\nand\nThumper".mb_chars) + end + + def test_bind_record + o = Struct.new(:quoted_id).new(1) + assert_equal '1', bind('?', o) + + os = [o] * 3 + assert_equal '1,1,1', bind('?', os) + end + + def test_named_bind_with_postgresql_type_casts + l = Proc.new { bind(":a::integer '2009-01-01'::date", :a => '10') } + assert_nothing_raised(&l) + assert_equal "#{ActiveRecord::Base.connection.quote('10')}::integer '2009-01-01'::date", l.call + end + + private + def bind(statement, *vars) + if vars.first.is_a?(Hash) + ActiveRecord::Base.send(:replace_named_bind_variables, statement, vars.first) + else + ActiveRecord::Base.send(:replace_bind_variables, statement, vars) + end + end end diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb index 2a2c2bc8d0..43f133b12d 100644 --- a/activerecord/test/cases/schema_dumper_test.rb +++ b/activerecord/test/cases/schema_dumper_test.rb @@ -353,6 +353,38 @@ class SchemaDumperTest < ActiveRecord::TestCase ActiveRecord::Base.table_name_suffix = ActiveRecord::Base.table_name_prefix = '' $stdout = original end + + def test_schema_dump_with_table_name_prefix_and_ignoring_tables + original, $stdout = $stdout, StringIO.new + + create_cat_migration = Class.new(ActiveRecord::Migration) do + def change + create_table("cats") do |t| + end + create_table("omg_cats") do |t| + end + end + end + + original_table_name_prefix = ActiveRecord::Base.table_name_prefix + original_schema_dumper_ignore_tables = ActiveRecord::SchemaDumper.ignore_tables + ActiveRecord::Base.table_name_prefix = 'omg_' + ActiveRecord::SchemaDumper.ignore_tables = ["cats"] + migration = create_cat_migration.new + migration.migrate(:up) + + stream = StringIO.new + output = ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, stream).string + + assert_match %r{create_table "omg_cats"}, output + refute_match %r{create_table "cats"}, output + ensure + migration.migrate(:down) + ActiveRecord::Base.table_name_prefix = original_table_name_prefix + ActiveRecord::SchemaDumper.ignore_tables = original_schema_dumper_ignore_tables + + $stdout = original + end end class SchemaDumperDefaultsTest < ActiveRecord::TestCase diff --git a/activerecord/test/cases/tasks/postgresql_rake_test.rb b/activerecord/test/cases/tasks/postgresql_rake_test.rb index c31f94b2f2..ba53f340ae 100644 --- a/activerecord/test/cases/tasks/postgresql_rake_test.rb +++ b/activerecord/test/cases/tasks/postgresql_rake_test.rb @@ -212,7 +212,7 @@ module ActiveRecord def test_structure_dump_with_schema_search_path @configuration['schema_search_path'] = 'foo,bar' - Kernel.expects(:system).with('pg_dump', '-s', '-x', '-O', '-f', @filename, '--schema=foo --schema=bar', 'my-app-db').returns(true) + Kernel.expects(:system).with('pg_dump', '-s', '-x', '-O', '-f', @filename, '--schema=foo', '--schema=bar', 'my-app-db').returns(true) ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, @filename) end @@ -228,7 +228,7 @@ module ActiveRecord end def test_structure_dump_with_dump_schemas_string - Kernel.expects(:system).with("pg_dump", '-s', '-x', '-O', '-f', @filename, '--schema=foo --schema=bar', "my-app-db").returns(true) + Kernel.expects(:system).with("pg_dump", '-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) diff --git a/activerecord/test/cases/time_precision_test.rb b/activerecord/test/cases/time_precision_test.rb index ff7a81fe60..3b6e4dcc2b 100644 --- a/activerecord/test/cases/time_precision_test.rb +++ b/activerecord/test/cases/time_precision_test.rb @@ -10,6 +10,7 @@ class TimePrecisionTest < ActiveRecord::TestCase setup do @connection = ActiveRecord::Base.connection + Foo.reset_column_information end teardown do @@ -20,8 +21,8 @@ class TimePrecisionTest < ActiveRecord::TestCase @connection.create_table(:foos, force: true) @connection.add_column :foos, :start, :time, precision: 3 @connection.add_column :foos, :finish, :time, precision: 6 - assert_equal 3, activerecord_column_option('foos', 'start', 'precision') - assert_equal 6, activerecord_column_option('foos', 'finish', 'precision') + assert_equal 3, Foo.columns_hash['start'].precision + assert_equal 6, Foo.columns_hash['finish'].precision end def test_passing_precision_to_time_does_not_set_limit @@ -29,8 +30,8 @@ class TimePrecisionTest < ActiveRecord::TestCase t.time :start, precision: 3 t.time :finish, precision: 6 end - assert_nil activerecord_column_option('foos', 'start', 'limit') - assert_nil activerecord_column_option('foos', 'finish', 'limit') + assert_nil Foo.columns_hash['start'].limit + assert_nil Foo.columns_hash['finish'].limit end def test_invalid_time_precision_raises_error @@ -42,15 +43,6 @@ class TimePrecisionTest < ActiveRecord::TestCase end end - def test_database_agrees_with_activerecord_about_precision - @connection.create_table(:foos, force: true) do |t| - t.time :start, precision: 2 - t.time :finish, precision: 4 - end - assert_equal 2, database_datetime_precision('foos', 'start') - assert_equal 4, database_datetime_precision('foos', 'finish') - end - def test_formatting_time_according_to_precision @connection.create_table(:foos, force: true) do |t| t.time :start, precision: 0 @@ -88,21 +80,5 @@ class TimePrecisionTest < ActiveRecord::TestCase end end - private - - def database_datetime_precision(table_name, column_name) - results = @connection.exec_query("SELECT column_name, datetime_precision FROM information_schema.columns WHERE table_name = '#{table_name}'") - result = results.find do |result_hash| - result_hash["column_name"] == column_name - end - result && result["datetime_precision"].to_i - end - - def activerecord_column_option(tablename, column_name, option) - result = @connection.columns(tablename).find do |column| - column.name == column_name - end - result && result.send(option) - end end end diff --git a/activerecord/test/cases/touch_later_test.rb b/activerecord/test/cases/touch_later_test.rb index 7058f4fbe2..b47769eed7 100644 --- a/activerecord/test/cases/touch_later_test.rb +++ b/activerecord/test/cases/touch_later_test.rb @@ -95,16 +95,14 @@ class TouchLaterTest < ActiveRecord::TestCase end def test_touching_three_deep - skip "Pending from #19324" - previous_tree_updated_at = trees(:root).updated_at previous_grandparent_updated_at = nodes(:grandparent).updated_at previous_parent_updated_at = nodes(:parent_a).updated_at previous_child_updated_at = nodes(:child_one_of_a).updated_at - travel 5.seconds - - Node.create! parent: nodes(:child_one_of_a), tree: trees(:root) + travel 5.seconds do + Node.create! parent: nodes(:child_one_of_a), tree: trees(:root) + end assert_not_equal nodes(:child_one_of_a).reload.updated_at, previous_child_updated_at assert_not_equal nodes(:parent_a).reload.updated_at, previous_parent_updated_at diff --git a/activerecord/test/cases/transaction_callbacks_test.rb b/activerecord/test/cases/transaction_callbacks_test.rb index f2229939c8..637f89196e 100644 --- a/activerecord/test/cases/transaction_callbacks_test.rb +++ b/activerecord/test/cases/transaction_callbacks_test.rb @@ -35,9 +35,9 @@ class TransactionCallbacksTest < ActiveRecord::TestCase has_many :replies, class_name: "ReplyWithCallbacks", foreign_key: "parent_id" after_commit { |record| record.do_after_commit(nil) } - after_commit(on: :create) { |record| record.do_after_commit(:create) } - after_commit(on: :update) { |record| record.do_after_commit(:update) } - after_commit(on: :destroy) { |record| record.do_after_commit(:destroy) } + after_create_commit { |record| record.do_after_commit(:create) } + after_update_commit { |record| record.do_after_commit(:update) } + after_destroy_commit { |record| record.do_after_commit(:destroy) } after_rollback { |record| record.do_after_rollback(nil) } after_rollback(on: :create) { |record| record.do_after_rollback(:create) } after_rollback(on: :update) { |record| record.do_after_rollback(:update) } diff --git a/activerecord/test/cases/validations/association_validation_test.rb b/activerecord/test/cases/validations/association_validation_test.rb index bff5ffa65e..584a3dc0d8 100644 --- a/activerecord/test/cases/validations/association_validation_test.rb +++ b/activerecord/test/cases/validations/association_validation_test.rb @@ -45,6 +45,18 @@ class AssociationValidationTest < ActiveRecord::TestCase assert t.valid? end + def test_validates_associated_without_marked_for_destruction + reply = Class.new do + def valid? + true + end + end + Topic.validates_associated(:replies) + t = Topic.new + t.define_singleton_method(:replies) { [reply.new] } + assert t.valid? + end + def test_validates_associated_with_custom_message_using_quotes Reply.validates_associated :topic, :message=> "This string contains 'single' and \"double\" quotes" Topic.validates_presence_of :content diff --git a/activerecord/test/cases/view_test.rb b/activerecord/test/cases/view_test.rb index e80d8bd584..d50ae74e35 100644 --- a/activerecord/test/cases/view_test.rb +++ b/activerecord/test/cases/view_test.rb @@ -45,7 +45,7 @@ module ViewBehavior def test_table_exists view_name = Ebook.table_name # TODO: switch this assertion around once we changed #tables to not return views. - assert @connection.table_exists?(view_name), "'#{view_name}' table should exist" + ActiveSupport::Deprecation.silence { assert @connection.table_exists?(view_name), "'#{view_name}' table should exist" } end def test_views_ara_valid_data_sources @@ -87,7 +87,7 @@ class ViewWithPrimaryKeyTest < ActiveRecord::TestCase end def drop_view(name) - @connection.execute "DROP VIEW #{name}" if @connection.table_exists? name + @connection.execute "DROP VIEW #{name}" if @connection.view_exists? name end end @@ -106,7 +106,7 @@ class ViewWithoutPrimaryKeyTest < ActiveRecord::TestCase end teardown do - @connection.execute "DROP VIEW paperbacks" if @connection.table_exists? "paperbacks" + @connection.execute "DROP VIEW paperbacks" if @connection.view_exists? "paperbacks" end def test_reading @@ -125,7 +125,8 @@ class ViewWithoutPrimaryKeyTest < ActiveRecord::TestCase def test_table_exists view_name = Paperback.table_name - assert @connection.table_exists?(view_name), "'#{view_name}' table should exist" + # TODO: switch this assertion around once we changed #tables to not return views. + ActiveSupport::Deprecation.silence { assert @connection.table_exists?(view_name), "'#{view_name}' table should exist" } end def test_column_definitions @@ -167,7 +168,7 @@ class UpdateableViewTest < ActiveRecord::TestCase end teardown do - @connection.execute "DROP VIEW printed_books" if @connection.table_exists? "printed_books" + @connection.execute "DROP VIEW printed_books" if @connection.view_exists? "printed_books" end def test_update_record @@ -209,8 +210,7 @@ class MaterializedViewTest < ActiveRecord::PostgreSQLTestCase end def drop_view(name) - @connection.execute "DROP MATERIALIZED VIEW #{name}" if @connection.table_exists? name - + @connection.execute "DROP MATERIALIZED VIEW #{name}" if @connection.view_exists? name end end end diff --git a/activerecord/test/models/bulb.rb b/activerecord/test/models/bulb.rb index a6e83fe353..dc0296305a 100644 --- a/activerecord/test/models/bulb.rb +++ b/activerecord/test/models/bulb.rb @@ -1,6 +1,7 @@ class Bulb < ActiveRecord::Base default_scope { where(:name => 'defaulty') } belongs_to :car, :touch => true + scope :awesome, -> { where(frickinawesome: true) } attr_reader :scope_after_initialize, :attributes_after_initialize @@ -49,3 +50,9 @@ class FailedBulb < Bulb throw(:abort) end end + +class TrickyBulb < Bulb + after_create do |record| + record.car.bulbs.to_a + end +end diff --git a/activerecord/test/models/car.rb b/activerecord/test/models/car.rb index 81263b79d1..778c22b1f6 100644 --- a/activerecord/test/models/car.rb +++ b/activerecord/test/models/car.rb @@ -4,6 +4,7 @@ class Car < ActiveRecord::Base has_many :funky_bulbs, class_name: 'FunkyBulb', dependent: :destroy has_many :failed_bulbs, class_name: 'FailedBulb', dependent: :destroy has_many :foo_bulbs, -> { where(:name => 'foo') }, :class_name => "Bulb" + has_many :awesome_bulbs, -> { awesome }, class_name: "Bulb" has_one :bulb diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index 66a1f5aa8a..025184f63a 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -114,7 +114,7 @@ ActiveRecord::Schema.define do create_table :bulbs, force: true do |t| t.integer :car_id t.string :name - t.boolean :frickinawesome + t.boolean :frickinawesome, default: false t.string :color end @@ -356,7 +356,7 @@ ActiveRecord::Schema.define do t.column :key, :string end - create_table :guitar, force: true do |t| + create_table :guitars, force: true do |t| t.string :color end diff --git a/activerecord/test/support/schema_dumping_helper.rb b/activerecord/test/support/schema_dumping_helper.rb index 2d1651454d..666c1b6a14 100644 --- a/activerecord/test/support/schema_dumping_helper.rb +++ b/activerecord/test/support/schema_dumping_helper.rb @@ -1,7 +1,7 @@ module SchemaDumpingHelper def dump_table_schema(table, connection = ActiveRecord::Base.connection) old_ignore_tables = ActiveRecord::SchemaDumper.ignore_tables - ActiveRecord::SchemaDumper.ignore_tables = connection.tables - [table] + ActiveRecord::SchemaDumper.ignore_tables = connection.data_sources - [table] stream = StringIO.new ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, stream) stream.string diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md index 7c0f3eae80..88558cde1c 100644 --- a/activesupport/CHANGELOG.md +++ b/activesupport/CHANGELOG.md @@ -1,3 +1,42 @@ +* `ActiveSupport::Cache::Store#namespaced_key`, + `ActiveSupport::Cache::MemCachedStore#escape_key`, and + `ActiveSupport::Cache::FileStore#key_file_path` + are deprecated and replaced with `normalize_key` that now calls `super`. + + `ActiveSupport::Cache::LocaleCache#set_cache_value` is deprecated and replaced with `write_cache_value`. + + *Michael Grosser* + +* Implements an evented file watcher to asynchronously detect changes in the + application source code, routes, locales, etc. + + This watcher is disabled by default, applications my enable it in the configuration: + + # config/environments/development.rb + config.file_watcher = ActiveSupport::EventedFileUpdateChecker + + This feature depends on the [listen](https://github.com/guard/listen) gem: + + group :development do + gem 'listen', '~> 3.0.5' + end + + *Puneet Agarwal* and *Xavier Noria* + +* Added `Time.days_in_year` to return the number of days in the given year, or the + current year if no argument is provided. + + *Jon Pascoe* + +* Updated `parameterize` to preserve the case of a string, optionally. + + Example: + + parameterize("Donald E. Knuth", separator: '_') # => "donald_e_knuth" + parameterize("Donald E. Knuth", preserve_case: true) # => "Donald-E-Knuth" + + *Swaathi Kakarla* + * `HashWithIndifferentAccess.new` respects the default value or proc on objects that respond to `#to_hash`. `.new_from_hash_copying_default` simply invokes `.new`. All calls to `.new_from_hash_copying_default` are replaced with `.new`. diff --git a/activesupport/README.rdoc b/activesupport/README.rdoc index cd72f53821..14ce204303 100644 --- a/activesupport/README.rdoc +++ b/activesupport/README.rdoc @@ -10,7 +10,7 @@ outside of Rails. The latest version of Active Support can be installed with RubyGems: - % gem install activesupport + $ gem install activesupport Source code can be downloaded as part of the Rails project on GitHub: @@ -37,4 +37,3 @@ Bug reports can be filed for the Ruby on Rails project here: Feature requests should be discussed on the rails-core mailing list here: * https://groups.google.com/forum/?fromgroups#!forum/rubyonrails-core - diff --git a/activesupport/activesupport.gemspec b/activesupport/activesupport.gemspec index 404c16b29f..32e28c0212 100644 --- a/activesupport/activesupport.gemspec +++ b/activesupport/activesupport.gemspec @@ -24,6 +24,6 @@ Gem::Specification.new do |s| s.add_dependency 'json', '~> 1.7', '>= 1.7.7' s.add_dependency 'tzinfo', '~> 1.1' s.add_dependency 'minitest', '~> 5.1' - s.add_dependency 'concurrent-ruby', '~> 1.0.0.pre5', '< 2.0.0' + s.add_dependency 'concurrent-ruby', '~> 1.0' s.add_dependency 'method_source' end diff --git a/activesupport/lib/active_support.rb b/activesupport/lib/active_support.rb index 63277a65b4..2019afeb00 100644 --- a/activesupport/lib/active_support.rb +++ b/activesupport/lib/active_support.rb @@ -34,6 +34,7 @@ module ActiveSupport autoload :Dependencies autoload :DescendantsTracker autoload :FileUpdateChecker + autoload :EventedFileUpdateChecker autoload :LogSubscriber autoload :Notifications diff --git a/activesupport/lib/active_support/cache.rb b/activesupport/lib/active_support/cache.rb index d0e53eaf05..5011014e96 100644 --- a/activesupport/lib/active_support/cache.rb +++ b/activesupport/lib/active_support/cache.rb @@ -8,6 +8,7 @@ require 'active_support/core_ext/numeric/bytes' require 'active_support/core_ext/numeric/time' require 'active_support/core_ext/object/to_param' require 'active_support/core_ext/string/inflections' +require 'active_support/core_ext/string/strip' module ActiveSupport # See ActiveSupport::Cache::Store for documentation. @@ -275,7 +276,7 @@ module ActiveSupport def fetch(name, options = nil) if block_given? options = merged_options(options) - key = namespaced_key(name, options) + key = normalize_key(name, options) instrument(:read, name, options) do |payload| cached_entry = read_entry(key, options) unless options[:force] @@ -302,7 +303,7 @@ module ActiveSupport # Options are passed to the underlying cache implementation. def read(name, options = nil) options = merged_options(options) - key = namespaced_key(name, options) + key = normalize_key(name, options) instrument(:read, name, options) do |payload| entry = read_entry(key, options) if entry @@ -334,7 +335,7 @@ module ActiveSupport instrument_multi(:read, names, options) do |payload| results = {} names.each do |name| - key = namespaced_key(name, options) + key = normalize_key(name, options) entry = read_entry(key, options) if entry if entry.expired? @@ -386,7 +387,7 @@ module ActiveSupport instrument(:write, name, options) do entry = Entry.new(value, options) - write_entry(namespaced_key(name, options), entry, options) + write_entry(normalize_key(name, options), entry, options) end end @@ -397,7 +398,7 @@ module ActiveSupport options = merged_options(options) instrument(:delete, name) do - delete_entry(namespaced_key(name, options), options) + delete_entry(normalize_key(name, options), options) end end @@ -408,7 +409,7 @@ module ActiveSupport options = merged_options(options) instrument(:exist?, name) do - entry = read_entry(namespaced_key(name, options), options) + entry = read_entry(normalize_key(name, options), options) (entry && !entry.expired?) || false end end @@ -529,7 +530,7 @@ module ActiveSupport # Prefix a key with the namespace. Namespace and key will be delimited # with a colon. - def namespaced_key(key, options) + def normalize_key(key, options) key = expanded_key(key) namespace = options[:namespace] if options prefix = namespace.is_a?(Proc) ? namespace.call : namespace @@ -537,8 +538,16 @@ module ActiveSupport key end + def namespaced_key(*args) + ActiveSupport::Deprecation.warn(<<-MESSAGE.strip_heredoc) + `namespaced_key` is deprecated and will be removed from Rails 5.1. + Please use `normalize_key` which will return a fully resolved key. + MESSAGE + normalize_key(*args) + end + def instrument(operation, key, options = nil) - log { "Cache #{operation}: #{namespaced_key(key, options)}#{options.blank? ? "" : " (#{options.inspect})"}" } + log { "Cache #{operation}: #{normalize_key(key, options)}#{options.blank? ? "" : " (#{options.inspect})"}" } payload = { :key => key } payload.merge!(options) if options.is_a?(Hash) diff --git a/activesupport/lib/active_support/cache/file_store.rb b/activesupport/lib/active_support/cache/file_store.rb index 9a88fc286a..dff2443bc8 100644 --- a/activesupport/lib/active_support/cache/file_store.rb +++ b/activesupport/lib/active_support/cache/file_store.rb @@ -60,7 +60,7 @@ module ActiveSupport matcher = key_matcher(matcher, options) search_dir(cache_path) do |path| key = file_path_key(path) - delete_entry(key, options) if key.match(matcher) + delete_entry(path, options) if key.match(matcher) end end end @@ -68,9 +68,8 @@ module ActiveSupport protected def read_entry(key, options) - file_name = key_file_path(key) - if File.exist?(file_name) - File.open(file_name) { |f| Marshal.load(f) } + if File.exist?(key) + File.open(key) { |f| Marshal.load(f) } end rescue => e logger.error("FileStoreError (#{e}): #{e.message}") if logger @@ -78,23 +77,21 @@ module ActiveSupport end def write_entry(key, entry, options) - file_name = key_file_path(key) - return false if options[:unless_exist] && File.exist?(file_name) - ensure_cache_path(File.dirname(file_name)) - File.atomic_write(file_name, cache_path) {|f| Marshal.dump(entry, f)} + return false if options[:unless_exist] && File.exist?(key) + ensure_cache_path(File.dirname(key)) + File.atomic_write(key, cache_path) {|f| Marshal.dump(entry, f)} true end def delete_entry(key, options) - file_name = key_file_path(key) - if File.exist?(file_name) + if File.exist?(key) begin - File.delete(file_name) - delete_empty_directories(File.dirname(file_name)) + File.delete(key) + delete_empty_directories(File.dirname(key)) true rescue => e # Just in case the error was caused by another process deleting the file first. - raise e if File.exist?(file_name) + raise e if File.exist?(key) false end end @@ -118,7 +115,8 @@ module ActiveSupport end # Translate a key into a file path. - def key_file_path(key) + def normalize_key(key, options) + key = super fname = URI.encode_www_form_component(key) if fname.size > FILEPATH_MAX_SIZE @@ -139,6 +137,14 @@ module ActiveSupport File.join(cache_path, DIR_FORMATTER % dir_1, DIR_FORMATTER % dir_2, *fname_paths) end + def key_file_path(key) + ActiveSupport::Deprecation.warn(<<-MESSAGE.strip_heredoc) + `key_file_path` is deprecated and will be removed from Rails 5.1. + Please use `normalize_key` which will return a fully resolved key or nothing. + MESSAGE + key + end + # Translate a file path into a key. def file_path_key(path) fname = path[cache_path.to_s.size..-1].split(File::SEPARATOR, 4).last @@ -175,7 +181,7 @@ module ActiveSupport # Modifies the amount of an already existing integer value that is stored in the cache. # If the key is not found nothing is done. def modify_value(name, amount, options) - file_name = key_file_path(namespaced_key(name, options)) + file_name = normalize_key(name, options) lock_file(file_name) do options = merged_options(options) diff --git a/activesupport/lib/active_support/cache/mem_cache_store.rb b/activesupport/lib/active_support/cache/mem_cache_store.rb index e2f536ef1e..174913365a 100644 --- a/activesupport/lib/active_support/cache/mem_cache_store.rb +++ b/activesupport/lib/active_support/cache/mem_cache_store.rb @@ -36,13 +36,13 @@ module ActiveSupport end def write_entry(key, entry, options) # :nodoc: - retval = super - if options[:raw] && local_cache && retval + if options[:raw] && local_cache raw_entry = Entry.new(entry.value.to_s) raw_entry.expires_at = entry.expires_at - local_cache.write_entry(key, raw_entry, options) + super(key, raw_entry, options) + else + super end - retval end end @@ -97,7 +97,7 @@ module ActiveSupport options = merged_options(options) instrument_multi(:read, names, options) do - keys_to_names = Hash[names.map{|name| [escape_key(namespaced_key(name, options)), name]}] + keys_to_names = Hash[names.map{|name| [normalize_key(name, options), name]}] raw_values = @data.get_multi(keys_to_names.keys, :raw => true) values = {} raw_values.each do |key, value| @@ -115,11 +115,10 @@ module ActiveSupport def increment(name, amount = 1, options = nil) # :nodoc: options = merged_options(options) instrument(:increment, name, :amount => amount) do - @data.incr(escape_key(namespaced_key(name, options)), amount) + rescue_error_with nil do + @data.incr(normalize_key(name, options), amount) + end end - rescue Dalli::DalliError => e - logger.error("DalliError (#{e}): #{e.message}") if logger - nil end # Decrement a cached value. This method uses the memcached decr atomic @@ -129,20 +128,16 @@ module ActiveSupport def decrement(name, amount = 1, options = nil) # :nodoc: options = merged_options(options) instrument(:decrement, name, :amount => amount) do - @data.decr(escape_key(namespaced_key(name, options)), amount) + rescue_error_with nil do + @data.decr(normalize_key(name, options), amount) + end end - rescue Dalli::DalliError => e - logger.error("DalliError (#{e}): #{e.message}") if logger - nil end # Clear the entire cache on all memcached servers. This method should # be used with care when shared cache is being used. def clear(options = nil) - @data.flush_all - rescue Dalli::DalliError => e - logger.error("DalliError (#{e}): #{e.message}") if logger - nil + rescue_error_with(nil) { @data.flush_all } end # Get the statistics from the memcached servers. @@ -153,10 +148,7 @@ module ActiveSupport protected # Read an entry from the cache. def read_entry(key, options) # :nodoc: - deserialize_entry(@data.get(escape_key(key), options)) - rescue Dalli::DalliError => e - logger.error("DalliError (#{e}): #{e.message}") if logger - nil + rescue_error_with(nil) { deserialize_entry(@data.get(key, options)) } end # Write an entry to the cache. @@ -168,18 +160,14 @@ module ActiveSupport # Set the memcache expire a few minutes in the future to support race condition ttls on read expires_in += 5.minutes end - @data.send(method, escape_key(key), value, expires_in, options) - rescue Dalli::DalliError => e - logger.error("DalliError (#{e}): #{e.message}") if logger - false + rescue_error_with false do + @data.send(method, key, value, expires_in, options) + end end # Delete an entry from the cache. def delete_entry(key, options) # :nodoc: - @data.delete(escape_key(key)) - rescue Dalli::DalliError => e - logger.error("DalliError (#{e}): #{e.message}") if logger - false + rescue_error_with(false) { @data.delete(key) } end private @@ -187,22 +175,35 @@ module ActiveSupport # Memcache keys are binaries. So we need to force their encoding to binary # before applying the regular expression to ensure we are escaping all # characters properly. - def escape_key(key) - key = key.to_s.dup + def normalize_key(key, options) + key = super.dup key = key.force_encoding(Encoding::ASCII_8BIT) key = key.gsub(ESCAPE_KEY_CHARS){ |match| "%#{match.getbyte(0).to_s(16).upcase}" } key = "#{key[0, 213]}:md5:#{Digest::MD5.hexdigest(key)}" if key.size > 250 key end + def escape_key(key) + ActiveSupport::Deprecation.warn(<<-MESSAGE.strip_heredoc) + `escape_key` is deprecated and will be removed from Rails 5.1. + Please use `normalize_key` which will return a fully resolved key or nothing. + MESSAGE + key + end + def deserialize_entry(raw_value) if raw_value entry = Marshal.load(raw_value) rescue raw_value entry.is_a?(Entry) ? entry : Entry.new(entry) - else - nil end end + + def rescue_error_with(fallback) + yield + rescue Dalli::DalliError => e + logger.error("DalliError (#{e}): #{e.message}") if logger + fallback + end end end end diff --git a/activesupport/lib/active_support/cache/memory_store.rb b/activesupport/lib/active_support/cache/memory_store.rb index 90bb2c38c3..896c28ad8b 100644 --- a/activesupport/lib/active_support/cache/memory_store.rb +++ b/activesupport/lib/active_support/cache/memory_store.rb @@ -75,30 +75,12 @@ module ActiveSupport # Increment an integer value in the cache. def increment(name, amount = 1, options = nil) - synchronize do - options = merged_options(options) - if num = read(name, options) - num = num.to_i + amount - write(name, num, options) - num - else - nil - end - end + modify_value(name, amount, options) end # Decrement an integer value in the cache. def decrement(name, amount = 1, options = nil) - synchronize do - options = merged_options(options) - if num = read(name, options) - num = num.to_i - amount - write(name, num, options) - num - else - nil - end - end + modify_value(name, -amount, options) end def delete_matched(matcher, options = nil) @@ -167,6 +149,19 @@ module ActiveSupport !!entry end end + + private + + def modify_value(name, amount, options) + synchronize do + options = merged_options(options) + if num = read(name, options) + num = num.to_i + amount + write(name, num, options) + num + end + end + end end end end diff --git a/activesupport/lib/active_support/cache/strategy/local_cache.rb b/activesupport/lib/active_support/cache/strategy/local_cache.rb index 4e2d3e9875..df38dbcf11 100644 --- a/activesupport/lib/active_support/cache/strategy/local_cache.rb +++ b/activesupport/lib/active_support/cache/strategy/local_cache.rb @@ -48,10 +48,6 @@ module ActiveSupport @data.clear end - def fetch(*args, &block) - @data.fetch(*args, &block) - end - def read_entry(key, options) @data[key] end @@ -64,6 +60,10 @@ module ActiveSupport def delete_entry(key, options) !!@data.delete(key) end + + def fetch_entry(key, options = nil) # :nodoc: + @data.fetch(key) { @data[key] = yield } + end end # Use a local cache for the duration of block. @@ -79,35 +79,35 @@ module ActiveSupport end def clear(options = nil) # :nodoc: - local_cache.clear(options) if local_cache + return super unless cache = local_cache + cache.clear(options) super end def cleanup(options = nil) # :nodoc: - local_cache.clear(options) if local_cache + return super unless cache = local_cache + cache.clear(options) super end def increment(name, amount = 1, options = nil) # :nodoc: + return super unless local_cache value = bypass_local_cache{super} - set_cache_value(value, name, amount, options) + write_cache_value(name, value, options) value end def decrement(name, amount = 1, options = nil) # :nodoc: + return super unless local_cache value = bypass_local_cache{super} - set_cache_value(value, name, amount, options) + write_cache_value(name, value, options) value end protected def read_entry(key, options) # :nodoc: if cache = local_cache - cache.fetch(key) do - entry = super - cache.write_entry(key, entry, options) - entry - end + cache.fetch_entry(key) { super } else super end @@ -124,13 +124,21 @@ module ActiveSupport end def set_cache_value(value, name, amount, options) # :nodoc: - if local_cache - local_cache.mute do - if value - local_cache.write(name, value, options) - else - local_cache.delete(name, options) - end + ActiveSupport::Deprecation.warn(<<-MESSAGE.strip_heredoc) + `set_cache_value` is deprecated and will be removed from Rails 5.1. + Please use `write_cache_value` + MESSAGE + write_cache_value name, value, options + end + + def write_cache_value(name, value, options) # :nodoc: + name = normalize_key(name, options) + cache = local_cache + cache.mute do + if value + cache.write(name, value, options) + else + cache.delete(name, options) end end end diff --git a/activesupport/lib/active_support/core_ext/class/subclasses.rb b/activesupport/lib/active_support/core_ext/class/subclasses.rb index 3c4bfc5f1e..b0f9a8be34 100644 --- a/activesupport/lib/active_support/core_ext/class/subclasses.rb +++ b/activesupport/lib/active_support/core_ext/class/subclasses.rb @@ -3,7 +3,8 @@ require 'active_support/core_ext/module/reachable' class Class begin - ObjectSpace.each_object(Class.new) {} + # Test if this Ruby supports each_object against singleton_class + ObjectSpace.each_object(Numeric.singleton_class) {} def descendants # :nodoc: descendants = [] @@ -12,7 +13,7 @@ class Class end descendants end - rescue StandardError # JRuby + rescue StandardError # JRuby 9.0.4.0 and earlier def descendants # :nodoc: descendants = [] ObjectSpace.each_object(Class) do |k| diff --git a/activesupport/lib/active_support/core_ext/enumerable.rb b/activesupport/lib/active_support/core_ext/enumerable.rb index fc7531d088..8a74ad4d66 100644 --- a/activesupport/lib/active_support/core_ext/enumerable.rb +++ b/activesupport/lib/active_support/core_ext/enumerable.rb @@ -21,7 +21,7 @@ module Enumerable if block_given? map(&block).sum(identity) else - inject { |sum, element| sum + element } || identity + inject(:+) || identity end end diff --git a/activesupport/lib/active_support/core_ext/hash/keys.rb b/activesupport/lib/active_support/core_ext/hash/keys.rb index 07a282e8b6..8b2366c4b3 100644 --- a/activesupport/lib/active_support/core_ext/hash/keys.rb +++ b/activesupport/lib/active_support/core_ext/hash/keys.rb @@ -10,7 +10,7 @@ class Hash # # hash.transform_keys.with_index { |k, i| [k, i].join } # => {"name0"=>"Rob", "age1"=>"28"} def transform_keys - return enum_for(:transform_keys) unless block_given? + return enum_for(:transform_keys) { size } unless block_given? result = self.class.new each_key do |key| result[yield(key)] = self[key] @@ -21,7 +21,7 @@ class Hash # 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? + return enum_for(:transform_keys!) { size } unless block_given? keys.each do |key| self[yield(key)] = delete(key) end diff --git a/activesupport/lib/active_support/core_ext/hash/transform_values.rb b/activesupport/lib/active_support/core_ext/hash/transform_values.rb index 9ddb838774..7d507ac998 100644 --- a/activesupport/lib/active_support/core_ext/hash/transform_values.rb +++ b/activesupport/lib/active_support/core_ext/hash/transform_values.rb @@ -9,7 +9,7 @@ class Hash # # { a: 1, b: 2 }.transform_values.with_index { |v, i| [v, i].join.to_i } # => { a: 10, b: 21 } def transform_values - return enum_for(:transform_values) unless block_given? + return enum_for(:transform_values) { size } unless block_given? return {} if empty? result = self.class.new each do |key, value| @@ -21,7 +21,7 @@ class Hash # Destructively converts all values using the +block+ operations. # Same as +transform_values+ but modifies +self+. def transform_values! - return enum_for(:transform_values!) unless block_given? + return enum_for(:transform_values!) { size } unless block_given? each do |key, value| self[key] = yield(value) end diff --git a/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb b/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb index bf175a8a70..124f90dc0f 100644 --- a/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb +++ b/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb @@ -5,7 +5,7 @@ require 'active_support/core_ext/array/extract_options' # attributes. class Module # Defines a class attribute and creates a class and instance reader methods. - # The underlying the class variable is set to +nil+, if it is not previously + # The underlying class variable is set to +nil+, if it is not previously # defined. # # module HairColors diff --git a/activesupport/lib/active_support/core_ext/string/inflections.rb b/activesupport/lib/active_support/core_ext/string/inflections.rb index b2e713077c..cc71b8155d 100644 --- a/activesupport/lib/active_support/core_ext/string/inflections.rb +++ b/activesupport/lib/active_support/core_ext/string/inflections.rb @@ -164,8 +164,26 @@ class String # # <%= link_to(@person.name, person_path) %> # # => <a href="/person/1-donald-e-knuth">Donald E. Knuth</a> - def parameterize(sep = '-'.freeze) - ActiveSupport::Inflector.parameterize(self, sep) + # + # To preserve the case of the characters in a string, use the `preserve_case` argument. + # + # class Person + # def to_param + # "#{id}-#{name.parameterize(preserve_case: true)}" + # end + # end + # + # @person = Person.find(1) + # # => #<Person id: 1, name: "Donald E. Knuth"> + # + # <%= link_to(@person.name, person_path) %> + # # => <a href="/person/1-Donald-E-Knuth">Donald E. Knuth</a> + def parameterize(sep = :unused, separator: '-', preserve_case: false) + unless sep == :unused + ActiveSupport::Deprecation.warn("Passing the separator argument as a positional parameter is deprecated and will soon be removed. Use `separator: '#{sep}'` instead.") + separator = sep + end + ActiveSupport::Inflector.parameterize(self, separator: separator, preserve_case: preserve_case) end # Creates the name of a table like Rails does for models to table names. This method diff --git a/activesupport/lib/active_support/core_ext/time/calculations.rb b/activesupport/lib/active_support/core_ext/time/calculations.rb index 82e003fc3b..675db8a36b 100644 --- a/activesupport/lib/active_support/core_ext/time/calculations.rb +++ b/activesupport/lib/active_support/core_ext/time/calculations.rb @@ -26,6 +26,12 @@ class Time end end + # Returns the number of days in the given year. + # If no year is specified, it will use the current year. + def days_in_year(year = current.year) + days_in_month(2, year) + 337 + end + # Returns <tt>Time.zone.now</tt> when <tt>Time.zone</tt> or <tt>config.time_zone</tt> are set, otherwise just returns <tt>Time.now</tt>. def current ::Time.zone ? ::Time.zone.now : ::Time.now diff --git a/activesupport/lib/active_support/evented_file_update_checker.rb b/activesupport/lib/active_support/evented_file_update_checker.rb new file mode 100644 index 0000000000..315be85fb3 --- /dev/null +++ b/activesupport/lib/active_support/evented_file_update_checker.rb @@ -0,0 +1,150 @@ +require 'set' +require 'pathname' +require 'concurrent/atomic/atomic_boolean' + +module ActiveSupport + class EventedFileUpdateChecker #:nodoc: all + def initialize(files, dirs = {}, &block) + @ph = PathHelper.new + @files = files.map { |f| @ph.xpath(f) }.to_set + + @dirs = {} + dirs.each do |dir, exts| + @dirs[@ph.xpath(dir)] = Array(exts).map { |ext| @ph.normalize_extension(ext) } + end + + @block = block + @updated = Concurrent::AtomicBoolean.new(false) + @lcsp = @ph.longest_common_subpath(@dirs.keys) + + if (dtw = directories_to_watch).any? + # Loading listen triggers warnings. These are originated by a legit + # usage of attr_* macros for private attributes, but adds a lot of noise + # to our test suite. Thus, we lazy load it and disable warnings locally. + silence_warnings { require 'listen' } + Listen.to(*dtw, &method(:changed)).start + end + end + + def updated? + @updated.true? + end + + def execute + @updated.make_false + @block.call + end + + def execute_if_updated + if updated? + execute + true + end + end + + private + + def changed(modified, added, removed) + unless updated? + @updated.make_true if (modified + added + removed).any? { |f| watching?(f) } + end + end + + def watching?(file) + file = @ph.xpath(file) + + if @files.member?(file) + true + elsif file.directory? + false + else + ext = @ph.normalize_extension(file.extname) + + file.dirname.ascend do |dir| + if @dirs.fetch(dir, []).include?(ext) + break true + elsif dir == @lcsp || dir.root? + break false + end + end + end + end + + def directories_to_watch + dtw = (@files + @dirs.keys).map { |f| @ph.existing_parent(f) } + dtw.compact! + dtw.uniq! + + @ph.filter_out_descendants(dtw) + end + + class PathHelper + using Module.new { + refine Pathname do + def ascendant_of?(other) + self != other && other.ascend do |ascendant| + break true if self == ascendant + end + end + end + } + + def xpath(path) + Pathname.new(path).expand_path + end + + def normalize_extension(ext) + ext.to_s.sub(/\A\./, '') + end + + # Given a collection of Pathname objects returns the longest subpath + # common to all of them, or +nil+ if there is none. + def longest_common_subpath(paths) + return if paths.empty? + + lcsp = Pathname.new(paths[0]) + + paths[1..-1].each do |path| + until lcsp.ascendant_of?(path) + if lcsp.root? + # If we get here a root directory is not an ascendant of path. + # This may happen if there are paths in different drives on + # Windows. + return + else + lcsp = lcsp.parent + end + end + end + + lcsp + end + + # Returns the deepest existing ascendant, which could be the argument itself. + def existing_parent(dir) + dir.ascend do |ascendant| + break ascendant if ascendant.directory? + end + end + + # Filters out directories which are descendants of others in the collection (stable). + def filter_out_descendants(dirs) + return dirs if dirs.length < 2 + + dirs_sorted_by_nparts = dirs.sort_by { |dir| dir.each_filename.to_a.length } + descendants = [] + + until dirs_sorted_by_nparts.empty? + dir = dirs_sorted_by_nparts.shift + + dirs_sorted_by_nparts.reject! do |possible_descendant| + dir.ascendant_of?(possible_descendant) && descendants << possible_descendant + end + end + + # Array#- preserves order. + dirs - descendants + end + end + end +end diff --git a/activesupport/lib/active_support/file_update_checker.rb b/activesupport/lib/active_support/file_update_checker.rb index 78b627c286..1fa9335080 100644 --- a/activesupport/lib/active_support/file_update_checker.rb +++ b/activesupport/lib/active_support/file_update_checker.rb @@ -35,7 +35,7 @@ module ActiveSupport # This method must also receive a block that will be called once a path # changes. The array of files and list of directories cannot be changed # after FileUpdateChecker has been initialized. - def initialize(files, dirs={}, &block) + def initialize(files, dirs = {}, &block) @files = files.freeze @glob = compile_glob(dirs) @block = block diff --git a/activesupport/lib/active_support/i18n_railtie.rb b/activesupport/lib/active_support/i18n_railtie.rb index 6775eec34b..82aacf3b24 100644 --- a/activesupport/lib/active_support/i18n_railtie.rb +++ b/activesupport/lib/active_support/i18n_railtie.rb @@ -56,7 +56,7 @@ module I18n I18n.enforce_available_locales = enforce_available_locales directories = watched_dirs_with_extensions(reloadable_paths) - reloader = ActiveSupport::FileUpdateChecker.new(I18n.load_path.dup, directories) do + reloader = app.config.file_watcher.new(I18n.load_path.dup, directories) do I18n.load_path.keep_if { |p| File.exist?(p) } I18n.load_path |= reloadable_paths.map(&:existent).flatten diff --git a/activesupport/lib/active_support/inflector/transliterate.rb b/activesupport/lib/active_support/inflector/transliterate.rb index 103207fb63..871cfb8a72 100644 --- a/activesupport/lib/active_support/inflector/transliterate.rb +++ b/activesupport/lib/active_support/inflector/transliterate.rb @@ -68,29 +68,44 @@ module ActiveSupport # # parameterize("Donald E. Knuth") # => "donald-e-knuth" # parameterize("^trés|Jolie-- ") # => "tres-jolie" - def parameterize(string, sep = '-') + # + # To use a custom separator, override the `separator` argument. + # + # parameterize("Donald E. Knuth", separator: '_') # => "donald_e_knuth" + # parameterize("^trés|Jolie-- ", separator: '_') # => "tres_jolie" + # + # To preserve the case of the characters in a string, use the `preserve_case` argument. + # + # parameterize("Donald E. Knuth", preserve_case: true) # => "Donald-E-Knuth" + # parameterize("^trés|Jolie-- ", preserve_case: true) # => "tres-Jolie" + # + def parameterize(string, sep = :unused, separator: '-', preserve_case: false) + unless sep == :unused + ActiveSupport::Deprecation.warn("Passing the separator argument as a positional parameter is deprecated and will soon be removed. Use `separator: '#{sep}'` instead.") + separator = sep + end # Replace accented chars with their ASCII equivalents. parameterized_string = transliterate(string) # Turn unwanted chars into the separator. - parameterized_string.gsub!(/[^a-z0-9\-_]+/i, sep) + parameterized_string.gsub!(/[^a-z0-9\-_]+/i, separator) - unless sep.nil? || sep.empty? - if sep == "-".freeze + unless separator.nil? || separator.empty? + if separator == "-".freeze re_duplicate_separator = /-{2,}/ re_leading_trailing_separator = /^-|-$/i else - re_sep = Regexp.escape(sep) + re_sep = Regexp.escape(separator) re_duplicate_separator = /#{re_sep}{2,}/ re_leading_trailing_separator = /^#{re_sep}|#{re_sep}$/i end # No more than one of the separator in a row. - parameterized_string.gsub!(re_duplicate_separator, sep) + parameterized_string.gsub!(re_duplicate_separator, separator) # Remove leading/trailing separator. parameterized_string.gsub!(re_leading_trailing_separator, ''.freeze) end - - parameterized_string.downcase! + + parameterized_string.downcase! unless preserve_case parameterized_string end end diff --git a/activesupport/lib/active_support/message_encryptor.rb b/activesupport/lib/active_support/message_encryptor.rb index c82a13511e..2dde01c844 100644 --- a/activesupport/lib/active_support/message_encryptor.rb +++ b/activesupport/lib/active_support/message_encryptor.rb @@ -34,8 +34,8 @@ module ActiveSupport # Initialize a new MessageEncryptor. +secret+ must be at least as long as # the cipher key size. For the default 'aes-256-cbc' cipher, this is 256 # bits. If you are using a user-entered secret, you can generate a suitable - # key with <tt>OpenSSL::Digest::SHA256.new(user_secret).digest</tt> or - # similar. + # key by using <tt>ActiveSupport::KeyGenerator</tt> or a similar key + # derivation function. # # Options: # * <tt>:cipher</tt> - Cipher to use. Can be any cipher returned by diff --git a/activesupport/lib/active_support/message_verifier.rb b/activesupport/lib/active_support/message_verifier.rb index 64c5232cf4..854029bf83 100644 --- a/activesupport/lib/active_support/message_verifier.rb +++ b/activesupport/lib/active_support/message_verifier.rb @@ -15,7 +15,7 @@ module ActiveSupport # In the authentication filter: # # id, time = @verifier.verify(cookies[:remember_me]) - # if time < Time.now + # if Time.now < time # self.current_user = User.find(id) # end # diff --git a/activesupport/lib/active_support/notifications/fanout.rb b/activesupport/lib/active_support/notifications/fanout.rb index 7798c7ec60..c53f9c1039 100644 --- a/activesupport/lib/active_support/notifications/fanout.rb +++ b/activesupport/lib/active_support/notifications/fanout.rb @@ -42,8 +42,8 @@ module ActiveSupport listeners_for(name).each { |s| s.start(name, id, payload) } end - def finish(name, id, payload) - listeners_for(name).each { |s| s.finish(name, id, payload) } + def finish(name, id, payload, listeners = listeners_for(name)) + listeners.each { |s| s.finish(name, id, payload) } end def publish(name, *args) diff --git a/activesupport/lib/active_support/notifications/instrumenter.rb b/activesupport/lib/active_support/notifications/instrumenter.rb index 075ddc2382..67f2ee1a7f 100644 --- a/activesupport/lib/active_support/notifications/instrumenter.rb +++ b/activesupport/lib/active_support/notifications/instrumenter.rb @@ -15,14 +15,15 @@ module ActiveSupport # and publish it. Notice that events get sent even if an error occurs # in the passed-in block. def instrument(name, payload={}) - start name, payload + # some of the listeners might have state + listeners_state = start name, payload begin yield payload rescue Exception => e payload[:exception] = [e.class.name, e.message] raise e ensure - finish name, payload + finish_with_state listeners_state, name, payload end end @@ -36,6 +37,10 @@ module ActiveSupport @notifier.finish name, @id, payload end + def finish_with_state(listeners_state, name, payload) + @notifier.finish name, @id, payload, listeners_state + end + private def unique_id diff --git a/activesupport/lib/active_support/per_thread_registry.rb b/activesupport/lib/active_support/per_thread_registry.rb index 506dd950cb..a909a65bb6 100644 --- a/activesupport/lib/active_support/per_thread_registry.rb +++ b/activesupport/lib/active_support/per_thread_registry.rb @@ -1,3 +1,5 @@ +require 'active_support/core_ext/module/delegation' + module ActiveSupport # This module is used to encapsulate access to thread local variables. # diff --git a/activesupport/lib/active_support/values/time_zone.rb b/activesupport/lib/active_support/values/time_zone.rb index 6404f65612..7ca3592520 100644 --- a/activesupport/lib/active_support/values/time_zone.rb +++ b/activesupport/lib/active_support/values/time_zone.rb @@ -86,7 +86,8 @@ module ActiveSupport "Paris" => "Europe/Paris", "Amsterdam" => "Europe/Amsterdam", "Berlin" => "Europe/Berlin", - "Bern" => "Europe/Berlin", + "Bern" => "Europe/Zurich", + "Zurich" => "Europe/Zurich", "Rome" => "Europe/Rome", "Stockholm" => "Europe/Stockholm", "Vienna" => "Europe/Vienna", diff --git a/activesupport/test/caching_test.rb b/activesupport/test/caching_test.rb index 94e73b6df3..a1bd2d5356 100644 --- a/activesupport/test/caching_test.rb +++ b/activesupport/test/caching_test.rb @@ -416,7 +416,7 @@ module CacheStoreBehavior def test_race_condition_protection_skipped_if_not_defined @cache.write('foo', 'bar') - time = @cache.send(:read_entry, 'foo', {}).expires_at + time = @cache.send(:read_entry, @cache.send(:normalize_key, 'foo', {}), {}).expires_at Time.stub(:now, Time.at(time)) do result = @cache.fetch('foo') do @@ -518,6 +518,12 @@ module CacheStoreBehavior ensure ActiveSupport::Notifications.unsubscribe "cache_read.active_support" end + + def test_can_call_deprecated_namesaced_key + assert_deprecated "`namespaced_key` is deprecated" do + @cache.send(:namespaced_key, 111, {}) + end + end end # https://rails.lighthouseapp.com/projects/8994/tickets/6225-memcachestore-cant-deal-with-umlauts-and-special-characters @@ -633,6 +639,13 @@ module LocalCacheBehavior end end + def test_local_cache_fetch + @cache.with_local_cache do + @cache.send(:local_cache).write 'foo', 'bar' + assert_equal 'bar', @cache.send(:local_cache).fetch('foo') + end + end + def test_local_cache_of_write_nil @cache.with_local_cache do assert @cache.write('foo', nil) @@ -642,6 +655,14 @@ module LocalCacheBehavior end end + def test_local_cache_of_read_nil + @cache.with_local_cache do + assert_equal nil, @cache.read('foo') + @cache.send(:bypass_local_cache) { @cache.write 'foo', 'bar' } + assert_equal nil, @cache.read('foo') + end + end + def test_local_cache_of_delete @cache.with_local_cache do @cache.write('foo', 'bar') @@ -686,6 +707,15 @@ module LocalCacheBehavior app = @cache.middleware.new(app) app.call({}) end + + def test_can_call_deprecated_set_cache_value + @cache.with_local_cache do + assert_deprecated "`set_cache_value` is deprecated" do + @cache.send(:set_cache_value, 1, 'foo', :ignored, {}) + end + assert_equal 1, @cache.read('foo') + end + end end module AutoloadingCacheBehavior @@ -776,13 +806,13 @@ class FileStoreTest < ActiveSupport::TestCase end def test_key_transformation - key = @cache.send(:key_file_path, "views/index?id=1") + key = @cache.send(:normalize_key, "views/index?id=1", {}) assert_equal "views/index?id=1", @cache.send(:file_path_key, key) end def test_key_transformation_with_pathname FileUtils.touch(File.join(cache_dir, "foo")) - key = @cache_with_pathname.send(:key_file_path, "views/index?id=1") + key = @cache_with_pathname.send(:normalize_key, "views/index?id=1", {}) assert_equal "views/index?id=1", @cache_with_pathname.send(:file_path_key, key) end @@ -790,7 +820,7 @@ class FileStoreTest < ActiveSupport::TestCase # remain valid def test_filename_max_size key = "#{'A' * ActiveSupport::Cache::FileStore::FILENAME_MAX_SIZE}" - path = @cache.send(:key_file_path, key) + path = @cache.send(:normalize_key, key, {}) Dir::Tmpname.create(path) do |tmpname, n, opts| assert File.basename(tmpname+'.lock').length <= 255, "Temp filename too long: #{File.basename(tmpname+'.lock').length}" end @@ -800,7 +830,7 @@ class FileStoreTest < ActiveSupport::TestCase # If filename is 'AAAAB', where max size is 4, the returned path should be AAAA/B def test_key_transformation_max_filename_size key = "#{'A' * ActiveSupport::Cache::FileStore::FILENAME_MAX_SIZE}B" - path = @cache.send(:key_file_path, key) + path = @cache.send(:normalize_key, key, {}) assert path.split('/').all? { |dir_name| dir_name.size <= ActiveSupport::Cache::FileStore::FILENAME_MAX_SIZE} assert_equal 'B', File.basename(path) end @@ -851,6 +881,12 @@ class FileStoreTest < ActiveSupport::TestCase @cache.write(1, nil) assert_equal false, @cache.write(1, "aaaaaaaaaa", unless_exist: true) end + + def test_can_call_deprecated_key_file_path + assert_deprecated "`key_file_path` is deprecated" do + assert_equal 111, @cache.send(:key_file_path, 111) + end + end end class MemoryStoreTest < ActiveSupport::TestCase @@ -1025,6 +1061,12 @@ class MemCacheStoreTest < ActiveSupport::TestCase value << 'bingo' assert_not_equal value, @cache.read('foo') end + + def test_can_call_deprecated_escape_key + assert_deprecated "`escape_key` is deprecated" do + assert_equal 111, @cache.send(:escape_key, 111) + end + end end class NullStoreTest < ActiveSupport::TestCase @@ -1102,11 +1144,11 @@ class CacheStoreLoggerTest < ActiveSupport::TestCase def test_log_with_proc_namespace proc = Proc.new do "proc_namespace" - end + end @cache.fetch('foo', {:namespace => proc}) { 'bar' } assert_match %r{proc_namespace:foo}, @buffer.string end - + def test_mute_logging @cache.mute { @cache.fetch('foo') { 'bar' } } assert @buffer.string.blank? diff --git a/activesupport/test/core_ext/hash/transform_keys_test.rb b/activesupport/test/core_ext/hash/transform_keys_test.rb index 5a0b99e22c..99af274614 100644 --- a/activesupport/test/core_ext/hash/transform_keys_test.rb +++ b/activesupport/test/core_ext/hash/transform_keys_test.rb @@ -18,15 +18,17 @@ class TransformKeysTest < ActiveSupport::TestCase assert_same original, mapped end - test "transform_keys returns an Enumerator if no block is given" do + test "transform_keys returns a sized Enumerator if no block is given" do original = { a: 'a', b: 'b' } enumerator = original.transform_keys + assert_equal original.size, enumerator.size assert_equal Enumerator, enumerator.class end - test "transform_keys! returns an Enumerator if no block is given" do + test "transform_keys! returns a sized Enumerator if no block is given" do original = { a: 'a', b: 'b' } enumerator = original.transform_keys! + assert_equal original.size, enumerator.size assert_equal Enumerator, enumerator.class end diff --git a/activesupport/test/core_ext/hash/transform_values_test.rb b/activesupport/test/core_ext/hash/transform_values_test.rb index 7c33227dc0..114022fbaf 100644 --- a/activesupport/test/core_ext/hash/transform_values_test.rb +++ b/activesupport/test/core_ext/hash/transform_values_test.rb @@ -47,15 +47,17 @@ class TransformValuesTest < ActiveSupport::TestCase assert_nil mapped[:b] end - test "transform_values returns an Enumerator if no block is given" do + test "transform_values returns a sized Enumerator if no block is given" do original = { a: 'a', b: 'b' } enumerator = original.transform_values + assert_equal original.size, enumerator.size assert_equal Enumerator, enumerator.class end - test "transform_values! returns an Enumerator if no block is given" do + test "transform_values! returns a sized Enumerator if no block is given" do original = { a: 'a', b: 'b' } enumerator = original.transform_values! + assert_equal original.size, enumerator.size assert_equal Enumerator, enumerator.class end diff --git a/activesupport/test/core_ext/string_ext_test.rb b/activesupport/test/core_ext/string_ext_test.rb index 9cc7bb1a77..2e69816364 100644 --- a/activesupport/test/core_ext/string_ext_test.rb +++ b/activesupport/test/core_ext/string_ext_test.rb @@ -143,15 +143,49 @@ class StringInflectionsTest < ActiveSupport::TestCase end end + def test_string_parameterized_normal_preserve_case + StringToParameterizedPreserveCase.each do |normal, slugged| + assert_equal(normal.parameterize(preserve_case: true), slugged) + end + end + def test_string_parameterized_no_separator StringToParameterizeWithNoSeparator.each do |normal, slugged| - assert_equal(normal.parameterize(''), slugged) + assert_equal(normal.parameterize(separator: ''), slugged) + end + end + + def test_string_parameterized_no_separator_deprecated + StringToParameterizeWithNoSeparator.each do |normal, slugged| + assert_deprecated(/Passing the separator argument as a positional parameter is deprecated and will soon be removed. Use `separator: ''` instead./i) do + assert_equal(normal.parameterize(''), slugged) + end + end + end + + def test_string_parameterized_no_separator_preserve_case + StringToParameterizePreserveCaseWithNoSeparator.each do |normal, slugged| + assert_equal(normal.parameterize(separator: '', preserve_case: true), slugged) end end def test_string_parameterized_underscore StringToParameterizeWithUnderscore.each do |normal, slugged| - assert_equal(normal.parameterize('_'), slugged) + assert_equal(normal.parameterize(separator: '_'), slugged) + end + end + + def test_string_parameterized_underscore_deprecated + StringToParameterizeWithUnderscore.each do |normal, slugged| + assert_deprecated(/Passing the separator argument as a positional parameter is deprecated and will soon be removed. Use `separator: '_'` instead./i) do + assert_equal(normal.parameterize('_'), slugged) + end + end + end + + def test_string_parameterized_underscore_preserve_case + StringToParameterizePreserceCaseWithUnderscore.each do |normal, slugged| + assert_equal(normal.parameterize(separator: '_', preserve_case: true), slugged) end end diff --git a/activesupport/test/core_ext/time_ext_test.rb b/activesupport/test/core_ext/time_ext_test.rb index 2d0fb70a6b..e45df63fd5 100644 --- a/activesupport/test/core_ext/time_ext_test.rb +++ b/activesupport/test/core_ext/time_ext_test.rb @@ -617,6 +617,25 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase end end + def test_days_in_year_with_year + assert_equal 365, Time.days_in_year(2005) + assert_equal 366, Time.days_in_year(2004) + assert_equal 366, Time.days_in_year(2000) + assert_equal 365, Time.days_in_year(1900) + end + + def test_days_in_year_in_common_year_without_year_arg + Time.stub(:now, Time.utc(2007)) do + assert_equal 365, Time.days_in_year + end + end + + def test_days_in_year_in_leap_year_without_year_arg + Time.stub(:now, Time.utc(2008)) do + assert_equal 366, Time.days_in_year + end + end + def test_last_month_on_31st assert_equal Time.local(2004, 2, 29), Time.local(2004, 3, 31).last_month end diff --git a/activesupport/test/evented_file_update_checker_test.rb b/activesupport/test/evented_file_update_checker_test.rb new file mode 100644 index 0000000000..bc3f77bd54 --- /dev/null +++ b/activesupport/test/evented_file_update_checker_test.rb @@ -0,0 +1,155 @@ +require 'abstract_unit' +require 'pathname' +require 'file_update_checker_shared_tests' + +class EventedFileUpdateCheckerTest < ActiveSupport::TestCase + include FileUpdateCheckerSharedTests + + def setup + skip if ENV['LISTEN'] == '0' + super + end + + def new_checker(files = [], dirs = {}, &block) + ActiveSupport::EventedFileUpdateChecker.new(files, dirs, &block).tap do + wait + end + end + + def teardown + super + Listen.stop + end + + def wait + sleep 1 + end + + def touch(files) + super + wait # wait for the events to fire + end + + def rm_f(files) + super + wait + end +end + +class EventedFileUpdateCheckerPathHelperTest < ActiveSupport::TestCase + def pn(path) + Pathname.new(path) + end + + setup do + @ph = ActiveSupport::EventedFileUpdateChecker::PathHelper.new + end + + test '#xpath returns the expanded path as a Pathname object' do + assert_equal pn(__FILE__).expand_path, @ph.xpath(__FILE__) + end + + test '#normalize_extension returns a bare extension as is' do + assert_equal 'rb', @ph.normalize_extension('rb') + end + + test '#normalize_extension removes a leading dot' do + assert_equal 'rb', @ph.normalize_extension('.rb') + end + + test '#normalize_extension supports symbols' do + assert_equal 'rb', @ph.normalize_extension(:rb) + end + + test '#longest_common_subpath finds the longest common subpath, if there is one' do + paths = %w( + /foo/bar + /foo/baz + /foo/bar/baz/woo/zoo + ).map { |path| pn(path) } + + assert_equal pn('/foo'), @ph.longest_common_subpath(paths) + end + + test '#longest_common_subpath returns the root directory as an edge case' do + paths = %w( + /foo/bar + /foo/baz + /foo/bar/baz/woo/zoo + /wadus + ).map { |path| pn(path) } + + assert_equal pn('/'), @ph.longest_common_subpath(paths) + end + + test '#longest_common_subpath returns nil for an empty collection' do + assert_nil @ph.longest_common_subpath([]) + end + + test '#existing_parent returns the most specific existing ascendant' do + wd = Pathname.getwd + + assert_equal wd, @ph.existing_parent(wd) + assert_equal wd, @ph.existing_parent(wd.join('non-existing/directory')) + assert_equal pn('/'), @ph.existing_parent(pn('/non-existing/directory')) + end + + test '#filter_out_descendants returns the same collection if there are no descendants (empty)' do + assert_equal [], @ph.filter_out_descendants([]) + end + + test '#filter_out_descendants returns the same collection if there are no descendants (one)' do + assert_equal ['/foo'], @ph.filter_out_descendants(['/foo']) + end + + test '#filter_out_descendants returns the same collection if there are no descendants (several)' do + paths = %w( + /Rails.root/app/controllers + /Rails.root/app/models + /Rails.root/app/helpers + ).map { |path| pn(path) } + + assert_equal paths, @ph.filter_out_descendants(paths) + end + + test '#filter_out_descendants filters out descendants preserving order' do + paths = %w( + /Rails.root/app/controllers + /Rails.root/app/controllers/concerns + /Rails.root/app/models + /Rails.root/app/models/concerns + /Rails.root/app/helpers + ).map { |path| pn(path) } + + assert_equal paths.values_at(0, 2, 4), @ph.filter_out_descendants(paths) + end + + test '#filter_out_descendants works on path units' do + paths = %w( + /foo/bar + /foo/barrrr + ).map { |path| pn(path) } + + assert_equal paths, @ph.filter_out_descendants(paths) + end + + test '#filter_out_descendants deals correctly with the root directory' do + paths = %w( + / + /foo + /foo/bar + ).map { |path| pn(path) } + + assert_equal paths.values_at(0), @ph.filter_out_descendants(paths) + end + + test '#filter_out_descendants preserves duplicates' do + paths = %w( + /foo + /foo/bar + /foo + ).map { |path| pn(path) } + + assert_equal paths.values_at(0, 2), @ph.filter_out_descendants(paths) + end +end diff --git a/activesupport/test/file_update_checker_shared_tests.rb b/activesupport/test/file_update_checker_shared_tests.rb new file mode 100644 index 0000000000..100cbc9756 --- /dev/null +++ b/activesupport/test/file_update_checker_shared_tests.rb @@ -0,0 +1,241 @@ +require 'fileutils' + +module FileUpdateCheckerSharedTests + extend ActiveSupport::Testing::Declarative + include FileUtils + + def tmpdir + @tmpdir ||= Dir.mktmpdir(nil, __dir__) + end + + def tmpfile(name) + "#{tmpdir}/#{name}" + end + + def tmpfiles + @tmpfiles ||= %w(foo.rb bar.rb baz.rb).map { |f| tmpfile(f) } + end + + def teardown + FileUtils.rm_rf(@tmpdir) if defined? @tmpdir + end + + test 'should not execute the block if no paths are given' do + i = 0 + + checker = new_checker { i += 1 } + + assert !checker.execute_if_updated + assert_equal 0, i + end + + test 'should not execute the block if no files change' do + i = 0 + + FileUtils.touch(tmpfiles) + + checker = new_checker(tmpfiles) { i += 1 } + + assert !checker.execute_if_updated + assert_equal 0, i + end + + test 'should execute the block once when files are created' do + i = 0 + + checker = new_checker(tmpfiles) { i += 1 } + + touch(tmpfiles) + + assert checker.execute_if_updated + assert_equal 1, i + end + + test 'should execute the block once when files are modified' do + i = 0 + + FileUtils.touch(tmpfiles) + + checker = new_checker(tmpfiles) { i += 1 } + + touch(tmpfiles) + + assert checker.execute_if_updated + assert_equal 1, i + end + + test 'should execute the block once when files are deleted' do + i = 0 + + FileUtils.touch(tmpfiles) + + checker = new_checker(tmpfiles) { i += 1 } + + rm_f(tmpfiles) + + assert checker.execute_if_updated + assert_equal 1, i + end + + + test 'updated should become true when watched files are created' do + i = 0 + + checker = new_checker(tmpfiles) { i += 1 } + assert !checker.updated? + + touch(tmpfiles) + + assert checker.updated? + end + + + test 'updated should become true when watched files are modified' do + i = 0 + + FileUtils.touch(tmpfiles) + + checker = new_checker(tmpfiles) { i += 1 } + assert !checker.updated? + + touch(tmpfiles) + + assert checker.updated? + end + + test 'updated should become true when watched files are deleted' do + i = 0 + + FileUtils.touch(tmpfiles) + + checker = new_checker(tmpfiles) { i += 1 } + assert !checker.updated? + + rm_f(tmpfiles) + + assert checker.updated? + end + + test 'should be robust to handle files with wrong modified time' do + i = 0 + + FileUtils.touch(tmpfiles) + + now = Time.now + time = Time.mktime(now.year + 1, now.month, now.day) # wrong mtime from the future + File.utime(time, time, tmpfiles[0]) + + checker = new_checker(tmpfiles) { i += 1 } + + touch(tmpfiles[1..-1]) + + assert checker.execute_if_updated + assert_equal 1, i + end + + test 'should cache updated result until execute' do + i = 0 + + checker = new_checker(tmpfiles) { i += 1 } + assert !checker.updated? + + touch(tmpfiles) + + assert checker.updated? + checker.execute + assert !checker.updated? + end + + test 'should execute the block if files change in a watched directory one extension' do + i = 0 + + checker = new_checker([], tmpdir => :rb) { i += 1 } + + touch(tmpfile('foo.rb')) + + assert checker.execute_if_updated + assert_equal 1, i + end + + test 'should execute the block if files change in a watched directory several extensions' do + i = 0 + + checker = new_checker([], tmpdir => [:rb, :txt]) { i += 1 } + + touch(tmpfile('foo.rb')) + + assert checker.execute_if_updated + assert_equal 1, i + + touch(tmpfile('foo.txt')) + + assert checker.execute_if_updated + assert_equal 2, i + end + + test 'should not execute the block if the file extension is not watched' do + i = 0 + + checker = new_checker([], tmpdir => :txt) { i += 1 } + + touch(tmpfile('foo.rb')) + + assert !checker.execute_if_updated + assert_equal 0, i + end + + test 'does not assume files exist on instantiation' do + i = 0 + + non_existing = tmpfile('non_existing.rb') + checker = new_checker([non_existing]) { i += 1 } + + touch(non_existing) + + assert checker.execute_if_updated + assert_equal 1, i + end + + test 'detects files in new subdirectories' do + i = 0 + + checker = new_checker([], tmpdir => :rb) { i += 1 } + + subdir = tmpfile('subdir') + mkdir(subdir) + wait + + assert !checker.execute_if_updated + assert_equal 0, i + + touch("#{subdir}/nested.rb") + + assert checker.execute_if_updated + assert_equal 1, i + end + + test 'looked up extensions are inherited in subdirectories not listening to them' do + i = 0 + + subdir = tmpfile('subdir') + mkdir(subdir) + + checker = new_checker([], tmpdir => :rb, subdir => :txt) { i += 1 } + + touch(tmpfile('new.txt')) + + assert !checker.execute_if_updated + assert_equal 0, i + + # subdir does not look for Ruby files, but its parent tmpdir does. + touch("#{subdir}/nested.rb") + + assert checker.execute_if_updated + assert_equal 1, i + + touch("#{subdir}/nested.txt") + + assert checker.execute_if_updated + assert_equal 2, i + end +end diff --git a/activesupport/test/file_update_checker_test.rb b/activesupport/test/file_update_checker_test.rb index bd1df0f858..752f7836cd 100644 --- a/activesupport/test/file_update_checker_test.rb +++ b/activesupport/test/file_update_checker_test.rb @@ -1,112 +1,19 @@ require 'abstract_unit' -require 'fileutils' -require 'thread' +require 'file_update_checker_shared_tests' -MTIME_FIXTURES_PATH = File.expand_path("../fixtures", __FILE__) +class FileUpdateCheckerTest < ActiveSupport::TestCase + include FileUpdateCheckerSharedTests -class FileUpdateCheckerWithEnumerableTest < ActiveSupport::TestCase - FILES = %w(1.txt 2.txt 3.txt) - - def setup - FileUtils.mkdir_p("tmp_watcher") - FileUtils.touch(FILES) - end - - def teardown - FileUtils.rm_rf("tmp_watcher") - FileUtils.rm_rf(FILES) - end - - def test_should_not_execute_the_block_if_no_paths_are_given - i = 0 - checker = ActiveSupport::FileUpdateChecker.new([]){ i += 1 } - checker.execute_if_updated - assert_equal 0, i - end - - def test_should_not_invoke_the_block_if_no_file_has_changed - i = 0 - checker = ActiveSupport::FileUpdateChecker.new(FILES){ i += 1 } - 5.times { assert !checker.execute_if_updated } - assert_equal 0, i - end - - def test_should_invoke_the_block_if_a_file_has_changed - i = 0 - checker = ActiveSupport::FileUpdateChecker.new(FILES){ i += 1 } - sleep(1) - FileUtils.touch(FILES) - assert checker.execute_if_updated - assert_equal 1, i - end - - def test_should_be_robust_enough_to_handle_deleted_files - i = 0 - checker = ActiveSupport::FileUpdateChecker.new(FILES){ i += 1 } - FileUtils.rm(FILES) - assert checker.execute_if_updated - assert_equal 1, i - end - - def test_should_be_robust_to_handle_files_with_wrong_modified_time - i = 0 - now = Time.now - time = Time.mktime(now.year + 1, now.month, now.day) # wrong mtime from the future - File.utime time, time, FILES[2] - - checker = ActiveSupport::FileUpdateChecker.new(FILES){ i += 1 } - - sleep(1) - FileUtils.touch(FILES[0..1]) - - assert checker.execute_if_updated - assert_equal 1, i - end - - def test_should_cache_updated_result_until_execute - i = 0 - checker = ActiveSupport::FileUpdateChecker.new(FILES){ i += 1 } - assert !checker.updated? - - sleep(1) - FileUtils.touch(FILES) - - assert checker.updated? - checker.execute - assert !checker.updated? - end - - def test_should_invoke_the_block_if_a_watched_dir_changed_its_glob - i = 0 - checker = ActiveSupport::FileUpdateChecker.new([], "tmp_watcher" => [:txt]){ i += 1 } - FileUtils.cd "tmp_watcher" do - FileUtils.touch(FILES) - end - assert checker.execute_if_updated - assert_equal 1, i + def new_checker(files = [], dirs = {}, &block) + ActiveSupport::FileUpdateChecker.new(files, dirs, &block) end - def test_should_not_invoke_the_block_if_a_watched_dir_changed_its_glob - i = 0 - checker = ActiveSupport::FileUpdateChecker.new([], "tmp_watcher" => :rb){ i += 1 } - FileUtils.cd "tmp_watcher" do - FileUtils.touch(FILES) - end - assert !checker.execute_if_updated - assert_equal 0, i + def wait + # noop end - def test_should_not_block_if_a_strange_filename_used - FileUtils.mkdir_p("tmp_watcher/valid,yetstrange,path,") - FileUtils.touch(FILES.map { |file_name| "tmp_watcher/valid,yetstrange,path,/#{file_name}" }) - - test = Thread.new do - ActiveSupport::FileUpdateChecker.new([],"tmp_watcher/valid,yetstrange,path," => :txt) { i += 1 } - Thread.exit - end - test.priority = -1 - test.join(5) - - assert !test.alive? + def touch(files) + sleep 1 # let's wait a bit to ensure there's a new mtime + super end end diff --git a/activesupport/test/inflector_test.rb b/activesupport/test/inflector_test.rb index a0764f6d6b..06cd41c86d 100644 --- a/activesupport/test/inflector_test.rb +++ b/activesupport/test/inflector_test.rb @@ -269,14 +269,32 @@ class InflectorTest < ActiveSupport::TestCase def test_parameterize_with_custom_separator jruby_skip "UTF-8 to UTF8-MAC Converter is unavailable" StringToParameterizeWithUnderscore.each do |some_string, parameterized_string| - assert_equal(parameterized_string, ActiveSupport::Inflector.parameterize(some_string, '_')) + assert_equal(parameterized_string, ActiveSupport::Inflector.parameterize(some_string, separator: '_')) + end + end + + def test_parameterize_with_custom_separator_deprecated + jruby_skip "UTF-8 to UTF8-MAC Converter is unavailable" + StringToParameterizeWithUnderscore.each do |some_string, parameterized_string| + assert_deprecated(/Passing the separator argument as a positional parameter is deprecated and will soon be removed. Use `separator: '_'` instead./i) do + assert_equal(parameterized_string, ActiveSupport::Inflector.parameterize(some_string, '_')) + end end end def test_parameterize_with_multi_character_separator jruby_skip "UTF-8 to UTF8-MAC Converter is unavailable" StringToParameterized.each do |some_string, parameterized_string| - assert_equal(parameterized_string.gsub('-', '__sep__'), ActiveSupport::Inflector.parameterize(some_string, '__sep__')) + assert_equal(parameterized_string.gsub('-', '__sep__'), ActiveSupport::Inflector.parameterize(some_string, separator: '__sep__')) + end + end + + def test_parameterize_with_multi_character_separator_deprecated + jruby_skip "UTF-8 to UTF8-MAC Converter is unavailable" + StringToParameterized.each do |some_string, parameterized_string| + assert_deprecated(/Passing the separator argument as a positional parameter is deprecated and will soon be removed. Use `separator: '__sep__'` instead./i) do + assert_equal(parameterized_string.gsub('-', '__sep__'), ActiveSupport::Inflector.parameterize(some_string, '__sep__')) + end end end diff --git a/activesupport/test/inflector_test_cases.rb b/activesupport/test/inflector_test_cases.rb index e6898658b5..14fe97a986 100644 --- a/activesupport/test/inflector_test_cases.rb +++ b/activesupport/test/inflector_test_cases.rb @@ -174,6 +174,17 @@ module InflectorTestCases "Test with malformed utf8 \251" => "test-with-malformed-utf8" } + StringToParameterizedPreserveCase = { + "Donald E. Knuth" => "Donald-E-Knuth", + "Random text with *(bad)* characters" => "Random-text-with-bad-characters", + "Allow_Under_Scores" => "Allow_Under_Scores", + "Trailing bad characters!@#" => "Trailing-bad-characters", + "!@#Leading bad characters" => "Leading-bad-characters", + "Squeeze separators" => "Squeeze-separators", + "Test with + sign" => "Test-with-sign", + "Test with malformed utf8 \xA9" => "Test-with-malformed-utf8" + } + StringToParameterizeWithNoSeparator = { "Donald E. Knuth" => "donaldeknuth", "With-some-dashes" => "with-some-dashes", @@ -185,6 +196,17 @@ module InflectorTestCases "Test with malformed utf8 \251" => "testwithmalformedutf8" } + StringToParameterizePreserveCaseWithNoSeparator = { + "Donald E. Knuth" => "DonaldEKnuth", + "With-some-dashes" => "With-some-dashes", + "Random text with *(bad)* characters" => "Randomtextwithbadcharacters", + "Trailing bad characters!@#" => "Trailingbadcharacters", + "!@#Leading bad characters" => "Leadingbadcharacters", + "Squeeze separators" => "Squeezeseparators", + "Test with + sign" => "Testwithsign", + "Test with malformed utf8 \xA9" => "Testwithmalformedutf8" + } + StringToParameterizeWithUnderscore = { "Donald E. Knuth" => "donald_e_knuth", "Random text with *(bad)* characters" => "random_text_with_bad_characters", @@ -197,6 +219,18 @@ module InflectorTestCases "Test with malformed utf8 \251" => "test_with_malformed_utf8" } + StringToParameterizePreserceCaseWithUnderscore = { + "Donald E. Knuth" => "Donald_E_Knuth", + "Random text with *(bad)* characters" => "Random_text_with_bad_characters", + "With-some-dashes" => "With-some-dashes", + "Allow_Under_Scores" => "Allow_Under_Scores", + "Trailing bad characters!@#" => "Trailing_bad_characters", + "!@#Leading bad characters" => "Leading_bad_characters", + "Squeeze separators" => "Squeeze_separators", + "Test with + sign" => "Test_with_sign", + "Test with malformed utf8 \xA9" => "Test_with_malformed_utf8" + } + StringToParameterizedAndNormalized = { "Malmö" => "malmo", "Garçons" => "garcons", diff --git a/activesupport/test/multibyte_conformance_test.rb b/activesupport/test/multibyte_conformance_test.rb index 2a885e32bf..d493a48fe4 100644 --- a/activesupport/test/multibyte_conformance_test.rb +++ b/activesupport/test/multibyte_conformance_test.rb @@ -104,11 +104,8 @@ class MultibyteConformanceTest < ActiveSupport::TestCase protected def each_line_of_norm_tests(&block) - lines = 0 - max_test_lines = 0 # Don't limit below 38, because that's the header of the testfile File.open(File.join(CACHE_DIR, UNIDATA_FILE), 'r') do | f | - until f.eof? || (max_test_lines > 38 and lines > max_test_lines) - lines += 1 + until f.eof? line = f.gets.chomp! next if (line.empty? || line =~ /^\#/) diff --git a/activesupport/test/notifications_test.rb b/activesupport/test/notifications_test.rb index f729f0a95b..d9cc392ac9 100644 --- a/activesupport/test/notifications_test.rb +++ b/activesupport/test/notifications_test.rb @@ -42,6 +42,21 @@ module Notifications ActiveSupport::Notifications.instrument(name) assert_equal expected, events end + + def test_subsribing_to_instrumentation_while_inside_it + # the repro requires that there are no evented subscribers for the "foo" event, + # so we have to duplicate some of the setup code + old_notifier = ActiveSupport::Notifications.notifier + ActiveSupport::Notifications.notifier = ActiveSupport::Notifications::Fanout.new + + ActiveSupport::Notifications.subscribe('foo', TestSubscriber.new) + + ActiveSupport::Notifications.instrument('foo') do + ActiveSupport::Notifications.subscribe('foo') {} + end + ensure + ActiveSupport::Notifications.notifier = old_notifier + end end class UnsubscribeTest < TestCase diff --git a/ci/travis.rb b/ci/travis.rb index 52fef05fbf..658d66c6b4 100755 --- a/ci/travis.rb +++ b/ci/travis.rb @@ -72,6 +72,10 @@ class Build key.join(':') end + def activesupport? + gem == 'activesupport' + end + def activerecord? gem == 'activerecord' end @@ -101,11 +105,22 @@ class Build tasks.each do |task| cmd = "bundle exec rake #{task}" puts "Running command: #{cmd}" - return false unless system(cmd) + return false unless system(env, cmd) end true end + def env + if activesupport? && !isolated? + # There is a known issue with the listen tests that casuses files to be + # incorrectly GC'ed even when they are still in-use. The current is to + # only run them in isolation to avoid randomly failing our test suite. + { 'LISTEN' => '0' } + else + {} + end + end + def run_bug_report_templates Dir.glob('bug_report_templates/*.rb').all? do |file| system(Gem.ruby, '-w', file) diff --git a/guides/rails_guides/generator.rb b/guides/rails_guides/generator.rb index b7a94f144c..7618fce2c8 100644 --- a/guides/rails_guides/generator.rb +++ b/guides/rails_guides/generator.rb @@ -162,7 +162,7 @@ module RailsGuides def select_only(guides) prefixes = ENV['ONLY'].split(",").map(&:strip) guides.select do |guide| - prefixes.any? { |p| guide.start_with?(p) || guide.start_with?("kindle") } + guide.start_with?('kindle'.freeze, *prefixes) end end diff --git a/guides/rails_guides/kindle.rb b/guides/rails_guides/kindle.rb index 32926622e3..081afcb09f 100644 --- a/guides/rails_guides/kindle.rb +++ b/guides/rails_guides/kindle.rb @@ -27,7 +27,7 @@ module Kindle generate_document_metadata(mobi_outfile) - puts "Creating MOBI document with kindlegen. This make take a while." + puts "Creating MOBI document with kindlegen. This may take a while." cmd = "kindlerb . > #{File.absolute_path logfile} 2>&1" puts cmd system(cmd) diff --git a/guides/source/active_job_basics.md b/guides/source/active_job_basics.md index a114686f0f..e36c0f899f 100644 --- a/guides/source/active_job_basics.md +++ b/guides/source/active_job_basics.md @@ -83,7 +83,7 @@ Note that you can define `perform` with as many arguments as you want. Enqueue a job like so: ```ruby -# Enqueue a job to be performed as soon the queuing system is +# Enqueue a job to be performed as soon as the queuing system is # free. GuestsCleanupJob.perform_later guest ``` diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index 13989a3b33..b5ad3e9411 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -412,4 +412,23 @@ end NOTE: the `:on` option specifies when a callback will be fired. If you don't supply the `:on` option the callback will fire for every action. +Since using `after_commit` callback only on create, update or delete is +common, there are aliases for those operations: + +* `after_create_commit` +* `after_update_commit` +* `after_destroy_commit` + +```ruby +class PictureFile < ActiveRecord::Base + after_destroy_commit :delete_picture_file_from_disk + + def delete_picture_file_from_disk + if File.exist?(filepath) + File.delete(filepath) + end + end +end +``` + WARNING. The `after_commit` and `after_rollback` callbacks are guaranteed to be called for all models created, updated, or destroyed within a transaction block. If any exceptions are raised within one of these callbacks, they will be ignored so that they don't interfere with the other callbacks. As such, if your callback code could raise an exception, you'll need to rescue it and handle it appropriately within the callback. diff --git a/guides/source/active_record_migrations.md b/guides/source/active_record_migrations.md index 67881e6087..5aa5dc4f60 100644 --- a/guides/source/active_record_migrations.md +++ b/guides/source/active_record_migrations.md @@ -454,8 +454,6 @@ number of digits after the decimal point. are using a dynamic value (such as a date), the default will only be calculated the first time (i.e. on the date the migration is applied). * `index` Adds an index for the column. -* `required` Adds `required: true` for `belongs_to` associations and -`null: false` to the column in the migration. Some adapters may support additional options; see the adapter specific API docs for further information. diff --git a/guides/source/active_record_querying.md b/guides/source/active_record_querying.md index ec31fa9d67..ed1c3e7061 100644 --- a/guides/source/active_record_querying.md +++ b/guides/source/active_record_querying.md @@ -1374,8 +1374,15 @@ Client.unscoped.load This method removes all scoping and will do a normal query on the table. -Note that chaining `unscoped` with a `scope` does not work. In these cases, it is -recommended that you use the block form of `unscoped`: +```ruby +Client.unscoped.all +# SELECT "clients".* FROM "clients" + +Client.where(published: false).unscoped.all +# SELECT "clients".* FROM "clients" +``` + +`unscoped` can also accept a block. ```ruby Client.unscoped { @@ -1392,6 +1399,36 @@ You can specify an exclamation point (`!`) on the end of the dynamic finders to If you want to find both by name and locked, you can chain these finders together by simply typing "`and`" between the fields. For example, `Client.find_by_first_name_and_locked("Ryan", true)`. +Enums +----- + +The `enum` macro maps an integer column to a set of possible values. + +```ruby +class Book < ActiveRecord::Base + enum availability: [:available, :unavailable] +end +``` + +This will automatically create the corresponding [scopes](#scopes) to query the +model. Methods to transition between states and query the current state are also +added. + +```ruby +# Both examples below query just available books. +Book.available +# or +Book.where(availability: :available) + +book = Book.new(availability: :available) +book.available? # => true +book.unavailable! # => true +book.available? # => false +``` + +Read the full documentation about enums +[in the Rails API docs](http://api.rubyonrails.org/classes/ActiveRecord/Enum.html). + Understanding The Method Chaining --------------------------------- diff --git a/guides/source/active_record_validations.md b/guides/source/active_record_validations.md index dd4d9f55fa..ec31385077 100644 --- a/guides/source/active_record_validations.md +++ b/guides/source/active_record_validations.md @@ -457,21 +457,6 @@ class Person < ActiveRecord::Base end ``` -This helper counts characters by default, but you can split the value in a -different way using the `:tokenizer` option: - -```ruby -class Essay < ActiveRecord::Base - validates :content, length: { - minimum: 300, - maximum: 400, - tokenizer: lambda { |str| str.split(/\s+/) }, - too_short: "must have at least %{count} words", - too_long: "must have at most %{count} words" - } -end -``` - Note that the default error messages are plural (e.g., "is too short (minimum is %{count} characters)"). For this reason, when `:minimum` is 1 you should provide a personalized message or use `presence: true` instead. When @@ -792,7 +777,36 @@ Topic.create(title: nil).valid? # => true As you've already seen, the `:message` option lets you specify the message that will be added to the `errors` collection when validation fails. When this option is not used, Active Record will use the respective default error message -for each validation helper. +for each validation helper. The `:message` option accepts a `String` or `Proc`. + +A `String` `:message` value can optionally contain any/all of `%{value}`, +`%{attribute}`, and `%{model}` which will be dynamically replaced when +validation fails. + +A `Proc` `:message` value is given two arguments: a message key for i18n, and +a hash with `:model`, `:attribute`, and `:value` key-value pairs. + +```ruby +class Person < ActiveRecord::Base + # Hard-coded message + validates :name, presence: { message: "must be given please" } + + # Message with dynamic attribute value. %{value} will be replaced with + # the actual value of the attribute. %{attribute} and %{model} also + # available. + validates :age, numericality: { message: "%{value} seems wrong" } + + # Proc + validates :username, + uniqueness: { + # key = "activerecord.errors.models.person.attributes.username.taken" + # data = { model: "Person", attribute: "Username", value: <username> } + message: ->(key, data) do + "#{data[:value]} taken! Try again #{Time.zone.tomorrow}" + end + } +end +``` ### `:on` diff --git a/guides/source/active_support_core_extensions.md b/guides/source/active_support_core_extensions.md index f6fc255c24..06c5476d45 100644 --- a/guides/source/active_support_core_extensions.md +++ b/guides/source/active_support_core_extensions.md @@ -517,17 +517,17 @@ Extensions to `Module` 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`: +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 `ActionDispatch::IntegrationTest#process` this way in `test/test_helper.rb`: ```ruby -ActionController::TestCase.class_eval do +ActionDispatch::IntegrationTest.class_eval do # save a reference to the original process method alias_method :original_process, :process # now redefine process and delegate to original_process - def process(action, params=nil, session=nil, flash=nil, http_method='GET') + def process('GET', path, params: nil, headers: nil, env: nil, xhr: false) params = Hash[*params.map {|k, v| [k, v.to_s]}.flatten] - original_process(action, params, session, flash, http_method) + original_process('GET', path, params: params) end end ``` @@ -537,10 +537,10 @@ That's the method `get`, `post`, etc., delegate the work to. That technique has a risk, it could be the case that `:original_process` was taken. To try to avoid collisions people choose some label that characterizes what the chaining is about: ```ruby -ActionController::TestCase.class_eval do +ActionDispatch::IntegrationTest.class_eval do def process_with_stringified_params(...) params = Hash[*params.map {|k, v| [k, v.to_s]}.flatten] - process_without_stringified_params(action, params, session, flash, http_method) + process_without_stringified_params(method, path, params: params) end alias_method :process_without_stringified_params, :process alias_method :process, :process_with_stringified_params @@ -550,10 +550,10 @@ end The method `alias_method_chain` provides a shortcut for that pattern: ```ruby -ActionController::TestCase.class_eval do +ActionDispatch::IntegrationTest.class_eval do def process_with_stringified_params(...) params = Hash[*params.map {|k, v| [k, v.to_s]}.flatten] - process_without_stringified_params(action, params, session, flash, http_method) + process_without_stringified_params(method, path, params: params) end alias_method_chain :process, :stringified_params end @@ -1710,6 +1710,20 @@ The method `parameterize` normalizes its receiver in a way that can be used in p "Kurt Gödel".parameterize # => "kurt-godel" ``` +To preserve the case of the string, set the `preserve_case` argument to true. By default, `preserve_case` is set to false. + +```ruby +"John Smith".parameterize(preserve_case: true) # => "John-Smith" +"Kurt Gödel".parameterize(preserve_case: true) # => "Kurt-Godel" +``` + +To use a custom separator, override the `separator` argument. + +```ruby +"John Smith".parameterize(separator: "_") # => "john\_smith" +"Kurt Gödel".parameterize(separator: "_") # => "kurt\_godel" +``` + In fact, the result string is wrapped in an instance of `ActiveSupport::Multibyte::Chars`. NOTE: Defined in `active_support/core_ext/string/inflections.rb`. diff --git a/guides/source/api_app.md b/guides/source/api_app.md index feaaff166a..17695c5db0 100644 --- a/guides/source/api_app.md +++ b/guides/source/api_app.md @@ -163,6 +163,14 @@ class definition: config.api_only = true ``` +Optionally, in `config/environments/development.rb` add the following line +to render error responses using the API format (JSON by default) when it +is a local request: + +```ruby +config.debug_exception_response_format = :api +``` + Finally, inside `app/controllers/application_controller.rb`, instead of: ```ruby @@ -221,7 +229,7 @@ For instance, using the `stale?` method: ```ruby def show - @post = Post.find(params[:id]) + @post = Post.find(params[:id]) if stale?(last_modified: @post.updated_at) render json: @post @@ -240,7 +248,7 @@ cross-client caching in the call to `stale?`: ```ruby def show - @post = Post.find(params[:id]) + @post = Post.find(params[:id]) if stale?(last_modified: @post.updated_at, public: true) render json: @post diff --git a/guides/source/autoloading_and_reloading_constants.md b/guides/source/autoloading_and_reloading_constants.md index 2b6d7e4044..a39b975c3e 100644 --- a/guides/source/autoloading_and_reloading_constants.md +++ b/guides/source/autoloading_and_reloading_constants.md @@ -685,7 +685,7 @@ to trigger the heuristic is defined in the conflicting place. ### Automatic Modules When a module acts as a namespace, Rails does not require the application to -defines a file for it, a directory matching the namespace is enough. +define a file for it, a directory matching the namespace is enough. Suppose an application has a back office whose controllers are stored in `app/controllers/admin`. If the `Admin` module is not yet loaded when @@ -790,7 +790,7 @@ Constant Reloading When `config.cache_classes` is false Rails is able to reload autoloaded constants. -For example, in you're in a console session and edit some file behind the +For example, if you're in a console session and edit some file behind the scenes, the code can be reloaded with the `reload!` command: ``` diff --git a/guides/source/configuring.md b/guides/source/configuring.md index 28388e4957..ba2fb4c1cf 100644 --- a/guides/source/configuring.md +++ b/guides/source/configuring.md @@ -35,7 +35,7 @@ In general, the work of configuring Rails means configuring the components of Ra For example, the `config/application.rb` file includes this setting: ```ruby -config.autoload_paths += %W(#{config.root}/extras) +config.time_zone = 'Central Time (US & Canada)' ``` This is a setting for Rails itself. If you want to pass settings to individual Rails components, you can do so via the same `config` object in `config/application.rb`: @@ -98,7 +98,7 @@ application. Accepts a valid week day symbol (e.g. `:monday`). * `config.exceptions_app` sets the exceptions application invoked by the ShowException middleware when an exception happens. Defaults to `ActionDispatch::PublicExceptions.new(Rails.public_path)`. -* `config.file_watcher` the class used to detect file updates in the filesystem when `config.reload_classes_only_on_change` is true. Must conform to `ActiveSupport::FileUpdateChecker` API. +* `config.file_watcher` is the class used to detect file updates in the file system when `config.reload_classes_only_on_change` is true. Rails ships with `ActiveSupport::FileUpdateChecker`, the default, and `ActiveSupport::EventedFileUpdateChecker` (this one depends on the [listen](https://github.com/guard/listen) gem). Custom classes must conform to the `ActiveSupport::FileUpdateChecker` API. * `config.filter_parameters` used for filtering out the parameters that you don't want shown in the logs, such as passwords or credit card @@ -199,7 +199,7 @@ The full set of methods that can be used in this block are as follows: Every Rails application comes with a standard set of middleware which it uses in this order in the development environment: * `ActionDispatch::SSL` forces every request to be under HTTPS protocol. Will be available if `config.force_ssl` is set to `true`. Options passed to this can be configured by using `config.ssl_options`. -* `ActionDispatch::Static` is used to serve static assets. Disabled if `config.public_file_server.enabled` is `false`. Set `config.static_index` if you need to serve a static directory index file that is not named `index`. For example, to serve `main.html` instead of `index.html` for directory requests, set `config.static_index` to `"main"`. +* `ActionDispatch::Static` is used to serve static assets. Disabled if `config.public_file_server.enabled` is `false`. Set `config.public_file_server.index_name` if you need to serve a static directory index file that is not named `index`. For example, to serve `main.html` instead of `index.html` for directory requests, set `config.public_file_server.index_name` to `"main"`. * `Rack::Lock` wraps the app in mutex so it can only be called by a single thread at a time. Only enabled when `config.cache_classes` is `false`. * `ActiveSupport::Cache::Strategy::LocalCache` serves as a basic memory backed cache. This cache is not thread safe and is intended only for serving as a temporary memory cache for a single thread. * `Rack::Runtime` sets an `X-Runtime` header, containing the time (in seconds) taken to execute the request. @@ -345,6 +345,8 @@ The schema dumper adds one additional configuration option: * `config.action_controller.allow_forgery_protection` enables or disables CSRF protection. By default this is `false` in test mode and `true` in all other modes. +* `config.action_controller.forgery_protection_origin_check` configures whether the HTTP `Origin` header should be checked against the site's origin as an additional CSRF defense. + * `config.action_controller.relative_url_root` can be used to tell Rails that you are [deploying to a subdirectory](configuring.html#deploy-to-a-subdirectory-relative-url-root). The default is `ENV['RAILS_RELATIVE_URL_ROOT']`. * `config.action_controller.permit_all_parameters` sets all the parameters for mass assignment to be permitted by default. The default value is `false`. @@ -1043,9 +1045,9 @@ Below is a comprehensive list of all the initializers found in Rails in the orde * `action_mailer.compile_config_methods` Initializes methods for the config settings specified so that they are quicker to access. -* `set_load_path` This initializer runs before `bootstrap_hook`. Adds the `vendor`, `lib`, all directories of `app` and any paths specified by `config.load_paths` to `$LOAD_PATH`. +* `set_load_path` This initializer runs before `bootstrap_hook`. Adds paths specified by `config.load_paths` and all autoload paths to `$LOAD_PATH`. -* `set_autoload_paths` This initializer runs before `bootstrap_hook`. Adds all sub-directories of `app` and paths specified by `config.autoload_paths` to `ActiveSupport::Dependencies.autoload_paths`. +* `set_autoload_paths` This initializer runs before `bootstrap_hook`. Adds all sub-directories of `app` and paths specified by `config.autoload_paths`, `config.eager_load_paths` and `config.autoload_once_paths` to `ActiveSupport::Dependencies.autoload_paths`. * `add_routing_paths` Loads (by default) all `config/routes.rb` files (in the application and railties, including engines) and sets up the routes for the application. @@ -1149,3 +1151,25 @@ Disallow: / To block just specific pages, it's necessary to use a more complex syntax. Learn it on the [official documentation](http://www.robotstxt.org/robotstxt.html). + +Evented File System Monitor +--------------------------- + +If the [listen gem](https://github.com/guard/listen) is loaded Rails uses an +evented file system monitor to detect changes when `config.cache_classes` is +false: + +```ruby +group :development do + gem 'listen', '~> 3.0.4' +end +``` + +Otherwise, in every request Rails walks the application tree to check if +anything has changed. + +On Linux and Mac OS X no additional gems are needed, but some are required +[for *BSD](https://github.com/guard/listen#on-bsd) and +[for Windows](https://github.com/guard/listen#on-windows). + +Note that [some setups are unsupported](https://github.com/guard/listen#issues--limitations). diff --git a/guides/source/contributing_to_ruby_on_rails.md b/guides/source/contributing_to_ruby_on_rails.md index 53c3cbf80b..ed88ecf6ac 100644 --- a/guides/source/contributing_to_ruby_on_rails.md +++ b/guides/source/contributing_to_ruby_on_rails.md @@ -16,7 +16,7 @@ After reading this guide, you will know: Ruby on Rails is not "someone else's framework." Over the years, hundreds of people have contributed to Ruby on Rails ranging from a single character to massive architectural changes or significant documentation - all with the goal of making Ruby on Rails better for everyone. Even if you don't feel up to writing code or documentation yet, there are a variety of other ways that you can contribute, from reporting issues to testing patches. As mentioned in [Rails -README](https://github.com/rails/rails/blob/master/README.md), everyone interacting in Rails and its sub-projects' codebases, issue trackers, chat rooms, and mailing lists is expected to follow the Rails [code of conduct](https://github.com/rails/rails/blob/master/CODE_OF_CONDUCT.md). +README](https://github.com/rails/rails/blob/master/README.md), everyone interacting in Rails and its sub-projects' codebases, issue trackers, chat rooms, and mailing lists is expected to follow the Rails [code of conduct](http://rubyonrails.org/conduct/). -------------------------------------------------------------------------------- @@ -148,6 +148,42 @@ NOTE: To help our CI servers you should add [ci skip] to your documentation comm WARNING: Docrails has a very strict policy: no code can be touched whatsoever, no matter how trivial or small the change. Only RDoc and guides can be edited via docrails. Also, CHANGELOGs should never be edited in docrails. +Translating Rails Guides +------------------------ + +We are happy to have people volunteer to translate the Rails guides into their own language. +If you want to translate the Rails guides in your own language, follows these steps: + +* Fork the project (rails/rails). +* Add a source folder for your own language, for example: *guides/source/it-IT* for Italian. +* Copy the contents of *guides/source* into your own language directory and translate them. +* Do NOT translate the HTML files, as they are automatically generated. + +To generate the guides in HTML format cd into the *guides* direcotry then run (eg. for it-IT): + +```bash +$ bundle install +$ bundle exec rake guides:generate:html GUIDES_LANGUAGE=it-IT +``` + +This will generate the guides in an *output* directory. + +NOTE: The instructions are for Rails > 4. The Redcarpet Gem doesn't work with JRuby. + +Translation efforts we know about (various versions): + +* **Italian**: [https://github.com/rixlabs/docrails](https://github.com/rixlabs/docrails) +* **Spanish**: [http://wiki.github.com/gramos/docrails](http://wiki.github.com/gramos/docrails) +* **Polish**: [http://github.com/apohllo/docrails/tree/master](http://github.com/apohllo/docrails/tree/master) +* **French** : [http://github.com/railsfrance/docrails](http://github.com/railsfrance/docrails) +* **Czech** : [https://github.com/rubyonrails-cz/docrails/tree/czech](https://github.com/rubyonrails-cz/docrails/tree/czech) +* **Turkish** : [https://github.com/ujk/docrails/tree/master](https://github.com/ujk/docrails/tree/master) +* **Korean** : [https://github.com/rorlakr/rails-guides](https://github.com/rorlakr/rails-guides) +* **Simplified Chinese** : [https://github.com/ruby-china/guides](https://github.com/ruby-china/guides) +* **Traditional Chinese** : [https://github.com/docrails-tw/guides](https://github.com/docrails-tw/guides) +* **Russian** : [https://github.com/morsbox/rusrails](https://github.com/morsbox/rusrails) +* **Japanese** : [https://github.com/yasslab/railsguides.jp](https://github.com/yasslab/railsguides.jp) + Contributing to the Rails Code ------------------------------ diff --git a/guides/source/debugging_rails_applications.md b/guides/source/debugging_rails_applications.md index a05abb61d6..5424313b33 100644 --- a/guides/source/debugging_rails_applications.md +++ b/guides/source/debugging_rails_applications.md @@ -862,8 +862,8 @@ such as Valgrind. ### Valgrind -[Valgrind](http://valgrind.org/) is a Linux-only application for detecting -C-based memory leaks and race conditions. +[Valgrind](http://valgrind.org/) is an application for detecting C-based memory +leaks and race conditions. There are Valgrind tools that can automatically detect many memory management and threading bugs, and profile your programs in detail. For example, if a C diff --git a/guides/source/engines.md b/guides/source/engines.md index f961b799f1..a50ef9a95f 100644 --- a/guides/source/engines.md +++ b/guides/source/engines.md @@ -239,6 +239,27 @@ NOTE: The `ApplicationController` class inside an engine is named just like a Rails application in order to make it easier for you to convert your applications into engines. +NOTE: Because of the way that Ruby does constant lookup you may run into a situation +where your engine controller is inheriting from the main application controller and +not your engine's application controller. Ruby is able to resolve the `ApplicationController` constant, and therefore the autoloading mechanism is not triggered. See the section [When Constants Aren't Missed](autoloading_and_reloading_constants.html#when-constants-aren-t-missed) of the [Autoloading and Reloading Constants](autoloading_and_reloading_constants.html) guide for further details. The best way to prevent this from +happening is to use `require_dependency` to ensure that the engine's application +controller is loaded. For example: + +``` ruby +# app/controllers/blorgh/articles_controller.rb: +require_dependency "blorgh/application_controller" + +module Blorgh + class ArticlesController < ApplicationController + ... + end +end +``` + +WARNING: Don't use `require` because it will break the automatic reloading of classes +in the development environment - using `require_dependency` ensures that classes are +loaded and unloaded in the correct manner. + Lastly, the `app/views` directory contains a `layouts` folder, which contains a file at `blorgh/application.html.erb`. This file allows you to specify a layout for the engine. If this engine is to be used as a stand-alone engine, then you @@ -1012,9 +1033,9 @@ typical `GET` to a controller in a controller's functional test like this: ```ruby module Blorgh - class FooControllerTest < ActionController::TestCase + class FooControllerTest < ActionDispatch::IntegrationTest def test_index - get :index + get foos_url ... end end @@ -1028,13 +1049,13 @@ in your setup code: ```ruby module Blorgh - class FooControllerTest < ActionController::TestCase + class FooControllerTest < ActionDispatch::IntegrationTest setup do @routes = Engine.routes end def test_index - get :index + get foos_url ... end end diff --git a/guides/source/form_helpers.md b/guides/source/form_helpers.md index 0a6e2e5dba..93bb51557a 100644 --- a/guides/source/form_helpers.md +++ b/guides/source/form_helpers.md @@ -657,7 +657,7 @@ NOTE: If the user has not selected a file the corresponding parameter will be an ### Dealing with Ajax -Unlike other forms making an asynchronous file upload form is not as simple as providing `form_for` with `remote: true`. With an Ajax form the serialization is done by JavaScript running inside the browser and since JavaScript cannot read files from your hard drive the file cannot be uploaded. The most common workaround is to use an invisible iframe that serves as the target for the form submission. +Unlike other forms, making an asynchronous file upload form is not as simple as providing `form_for` with `remote: true`. With an Ajax form the serialization is done by JavaScript running inside the browser and since JavaScript cannot read files from your hard drive the file cannot be uploaded. The most common workaround is to use an invisible iframe that serves as the target for the form submission. Customizing Form Builders ------------------------- diff --git a/guides/source/i18n.md b/guides/source/i18n.md index 87d2fafaf3..8381636196 100644 --- a/guides/source/i18n.md +++ b/guides/source/i18n.md @@ -805,6 +805,8 @@ en: Then `User.human_attribute_name("gender.female")` will return "Female". +NOTE: If you are using a class which includes `ActiveModel` and does not inherit from `ActiveRecord::Base`, replace `activerecord` with `activemodel` in the above key paths. + #### Error Message Scopes Active Record validation error messages can also be translated easily. Active Record gives you a couple of namespaces where you can place your message translations in order to provide different messages and translation for certain models, attributes, and/or validations. It also transparently takes single table inheritance into account. diff --git a/guides/source/initialization.md b/guides/source/initialization.md index ebe1cb206a..7bf7eebb62 100644 --- a/guides/source/initialization.md +++ b/guides/source/initialization.md @@ -139,7 +139,8 @@ aliases = { "c" => "console", "s" => "server", "db" => "dbconsole", - "r" => "runner" + "r" => "runner", + "t" => "test" } command = ARGV.shift @@ -158,19 +159,20 @@ defined here to find the matching command. ### `rails/commands/command_tasks.rb` -When one types an incorrect rails command, the `run_command` is responsible for -throwing an error message. If the command is valid, a method of the same name -is called. +When one types a valid Rails command, `run_command!` a method of the same name +is called. If Rails doesn't recognize the command, it tries to run a Rake task +of the same name. ```ruby COMMAND_WHITELIST = %w(plugin generate destroy console server dbconsole application runner new version help) def run_command!(command) command = parse_command(command) + if COMMAND_WHITELIST.include?(command) send(command) else - write_error_message(command) + run_rake_task(command) end end ``` diff --git a/guides/source/plugins.md b/guides/source/plugins.md index b94c26a1ae..922bbb4f73 100644 --- a/guides/source/plugins.md +++ b/guides/source/plugins.md @@ -294,7 +294,7 @@ Getting closer... Now we will implement the code of the `acts_as_yaffle` method module Yaffle module ActsAsYaffle - extend ActiveSupport::Concern + extend ActiveSupport::Concern included do end diff --git a/guides/source/routing.md b/guides/source/routing.md index 245689932b..fc756d00b3 100644 --- a/guides/source/routing.md +++ b/guides/source/routing.md @@ -142,10 +142,10 @@ Sometimes, you have a resource that clients always look up without referencing a get 'profile', to: 'users#show' ``` -Passing a `String` to `get` will expect a `controller#action` format, while passing a `Symbol` will map directly to an action: +Passing a `String` to `get` will expect a `controller#action` format, while passing a `Symbol` will map directly to an action but you must also specify the `controller:` to use: ```ruby -get 'profile', to: :show +get 'profile', to: :show, controller: 'users' ``` This resourceful route: diff --git a/guides/source/testing.md b/guides/source/testing.md index 2f941a8280..58524fd6c5 100644 --- a/guides/source/testing.md +++ b/guides/source/testing.md @@ -54,10 +54,12 @@ NOTE: Your tests are run under `RAILS_ENV=test`. ### Rails meets Minitest -If you remember when you used the `rails generate scaffold` command from the [Getting Started with Rails](getting_started.html) guide. We created our first resource among other things it created test stubs in the `test` directory: +If you remember when you used the `rails generate model` command from the +[Getting Started with Rails](getting_started.html) guide. We created our first +model among other things it created test stubs in the `test` directory: ```bash -$ bin/rails generate scaffold article title:string body:text +$ bin/rails generate model article title:string body:text ... create app/models/article.rb create test/models/article_test.rb @@ -155,7 +157,7 @@ Failed assertion, no message given. 1 tests, 1 assertions, 1 failures, 0 errors, 0 skips ``` -In the output, `F` denotes a failure. You can see the corresponding trace shown under `1)` along with the name of the failing test. The next few lines contain the stack trace followed by a message which mentions the actual value and the expected value by the assertion. The default assertion messages provide just enough information to help pinpoint the error. To make the assertion failure message more readable, every assertion provides an optional message parameter, as shown here: +In the output, `F` denotes a failure. You can see the corresponding trace shown under `1)` along with the name of the failing test. The next few lines contain the stack trace followed by a message that mentions the actual value and the expected value by the assertion. The default assertion messages provide just enough information to help pinpoint the error. To make the assertion failure message more readable, every assertion provides an optional message parameter, as shown here: ```ruby test "should not save article without title" do @@ -328,7 +330,6 @@ You'll see the usage of some of these assertions in the next chapter. All the basic assertions such as `assert_equal` defined in `Minitest::Assertions` are also available in the classes we use in our own test cases. In fact, Rails provides the following classes for you to inherit from: * `ActiveSupport::TestCase` -* `ActionController::TestCase` * `ActionMailer::TestCase` * `ActionView::TestCase` * `ActionDispatch::IntegrationTest` @@ -523,7 +524,7 @@ Model tests don't have their own superclass like `ActionMailer::TestCase` instea Integration Testing ------------------- -Integration tests are used to test how various parts of your application interact. They are generally used to test important work flows within your application. +Integration tests are used to test how various parts of your application interact. They are generally used to test important workflows within your application. For creating Rails integration tests, we use the 'test/integration' directory for your application. Rails provides a generator to create an integration test skeleton for you. @@ -649,23 +650,40 @@ You should test for things such as: * was the correct object stored in the response template? * was the appropriate message displayed to the user in the view? -Now that we have used Rails scaffold generator for our `Article` resource, it has already created the controller code and tests. You can take look at the file `articles_controller_test.rb` in the `test/controllers` directory. +The easiest way to see functional tests in action is to generate a controller +scaffold: -The following command will generate a controller test case with a filled up -test for each of the seven default actions. +```bash +$ bin/rails generate scaffold_controller article title:string body:test +... +create app/controllers/articles_controller.rb +... +invoke test_unit +create test/controllers/articles_controller_test.rb +... +``` + +This will generate the controller code and tests for an `Article` resource. +You can take look at the file `articles_controller_test.rb` in the `test/controllers` directory. + +If you already have a controller and just want to generate the test scaffold code for +each of the seven default actions, you can use the following command: ```bash $ bin/rails generate test_unit:scaffold article +... +invoke test_unit create test/controllers/articles_controller_test.rb +... ``` Let me take you through one such test, `test_should_get_index` from the file `articles_controller_test.rb`. ```ruby # articles_controller_test.rb -class ArticlesControllerTest < ActionController::TestCase +class ArticlesControllerTest < ActionDispatch::IntegrationTest test "should get index" do - get :index + get '/articles' assert_response :success assert_includes @response.body, 'Articles' end @@ -678,7 +696,7 @@ and also ensuring that the right response body has been generated. The `get` method kicks off the web request and populates the results into the response. It accepts 4 arguments: * The action of the controller you are requesting. - This can be in the form of a string or a symbol. + This can be in the form of a string or a route (i.e. `articles_url`). * `params`: option with a hash of request parameters to pass into the action (e.g. query string parameters or article variables). @@ -698,7 +716,7 @@ 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_url, 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. @@ -708,7 +726,7 @@ Let us modify `test_should_create_article` test in `articles_controller_test.rb` ```ruby test "should create article" do assert_difference('Article.count') do - post :create, params: { article: { title: 'Some title' } } + post '/article', params: { article: { title: 'Some title' } } end assert_redirected_to article_path(Article.last) @@ -739,7 +757,8 @@ To test AJAX requests, you can specify the `xhr: true` option to `get`, `post`, ```ruby test "ajax request" do - get :show, params: { id: articles(:first).id }, xhr: true + article = articles(:first) + get article_url(article), xhr: true assert_equal 'hello world', @response.body assert_equal "text/javascript", @response.content_type @@ -780,11 +799,11 @@ can be set directly on the `@request` instance variable: ```ruby # setting a HTTP Header @request.headers["Accept"] = "text/plain, text/html" -get :index # simulate the request with custom header +get articles_url # simulate the request with custom header # setting a CGI variable @request.headers["HTTP_REFERER"] = "http://example.com/home" -post :create # simulate the request with custom env variable +post article_url # simulate the request with custom env variable ``` ### Testing `flash` notices @@ -799,7 +818,7 @@ Let's start by adding this assertion to our `test_should_create_article` test: ```ruby test "should create article" do assert_difference('Article.count') do - post :create, params: { article: { title: 'Some title' } } + post article_url, params: { article: { title: 'Some title' } } end assert_redirected_to article_path(Article.last) @@ -869,7 +888,7 @@ Let's write a test for the `:show` action: ```ruby test "should show article" do article = articles(:one) - get :show, params: { id: article.id } + get '/article', params: { id: article.id } assert_response :success end ``` @@ -882,7 +901,7 @@ How about deleting an existing Article? test "should destroy article" do article = articles(:one) assert_difference('Article.count', -1) do - delete :destroy, params: { id: article.id } + delete article_url(article) end assert_redirected_to articles_path @@ -894,7 +913,7 @@ We can also add a test for updating an existing Article. ```ruby test "should update article" do article = articles(:one) - patch :update, params: { id: article.id, article: { title: "updated" } } + patch '/article', params: { id: article.id, article: { title: "updated" } } assert_redirected_to article_path(article) end ``` @@ -906,7 +925,7 @@ Our test should now look something like this, disregard the other tests we're le ```ruby require 'test_helper' -class ArticlesControllerTest < ActionController::TestCase +class ArticlesControllerTest < ActionDispatch::IntegrationTest # called before every single test setup do @article = articles(:one) @@ -920,20 +939,20 @@ class ArticlesControllerTest < ActionController::TestCase test "should show article" do # Reuse the @article instance variable from setup - get :show, params: { id: @article.id } + get article_url(@article) assert_response :success end test "should destroy article" do assert_difference('Article.count', -1) do - delete :destroy, params: { id: @article.id } + delete article_url(@article) end assert_redirected_to articles_path end test "should update article" do - patch :update, params: { id: @article.id, article: { title: "updated" } } + patch article_url(@article), params: { article: { title: "updated" } } assert_redirected_to article_path(@article) end end @@ -955,7 +974,7 @@ module SignInHelper end end -class ActionController::TestCase +class ActionDispatch::IntegrationTest include SignInHelper end ``` @@ -963,13 +982,13 @@ end ```ruby require 'test_helper' -class ProfileControllerTest < ActionController::TestCase +class ProfileControllerTest < ActionDispatch::IntegrationTest test "should show profile" do # helper is now reusable from any controller test case sign_in users(:david) - get :show + get profile_url assert_response :success end end @@ -1167,10 +1186,10 @@ Functional testing for mailers involves more than just checking that the email b ```ruby require 'test_helper' -class UserControllerTest < ActionController::TestCase +class UserControllerTest < ActionDispatch::IntegrationTest test "invite friend" do assert_difference 'ActionMailer::Base.deliveries.size', +1 do - post :invite_friend, params: { email: 'friend@example.com' } + post invite_friend_url, params: { email: 'friend@example.com' } end invite_email = ActionMailer::Base.deliveries.last @@ -1232,3 +1251,24 @@ class ProductTest < ActiveJob::TestCase end end ``` + +Testing Time-Dependent Code +--------------------------- + +Rails provides inbuilt helper methods that enable you to assert that your time-sensitve code works as expected. + +Here is an example using the [`travel_to`](http://api.rubyonrails.org/classes/ActiveSupport/Testing/TimeHelpers.html#method-i-travel_to) helper: + +```ruby +# Lets say that a user is eligible for gifting a month after they register. +user = User.create(name: 'Gaurish', activation_date: Date.new(2004, 10, 24)) +assert_not user.applicable_for_gifting? +travel_to Date.new(2004, 11, 24) do + assert_equal Date.new(2004, 10, 24), user.activation_date # inside the travel_to block `Date.current` is mocked + assert user.applicable_for_gifting? +end +assert_equal Date.new(2004, 10, 24), user.activation_date # The change was visible only inside the `travel_to` block. +``` + +Please see [`ActiveSupport::TimeHelpers` API Documentation](http://api.rubyonrails.org/classes/ActiveSupport/Testing/TimeHelpers.html) +for in-depth information about the available time helpers. diff --git a/guides/source/working_with_javascript_in_rails.md b/guides/source/working_with_javascript_in_rails.md index 1c42ff2914..48fc6bc9c0 100644 --- a/guides/source/working_with_javascript_in_rails.md +++ b/guides/source/working_with_javascript_in_rails.md @@ -81,7 +81,7 @@ Awkward, right? We could pull the function definition out of the click handler, and turn it into CoffeeScript: ```coffeescript -paintIt = (element, backgroundColor, textColor) -> +@paintIt = (element, backgroundColor, textColor) -> element.style.backgroundColor = backgroundColor if textColor? element.style.color = textColor @@ -107,7 +107,7 @@ attribute to our link, and then bind a handler to the click event of every link that has that attribute: ```coffeescript -paintIt = (element, backgroundColor, textColor) -> +@paintIt = (element, backgroundColor, textColor) -> element.style.backgroundColor = backgroundColor if textColor? element.style.color = textColor diff --git a/railties/CHANGELOG.md b/railties/CHANGELOG.md index 6980ba94e2..b9d2db773a 100644 --- a/railties/CHANGELOG.md +++ b/railties/CHANGELOG.md @@ -1,3 +1,48 @@ +* The generated config file for the development environment includes a new + config line, commented out, showing how to enable the evented file watcher. + + *Xavier Noria* + +* `config.debug_exception_response_format` configures the format used + in responses when errors occur in development mode. + + Set `config.debug_exception_response_format` to render an HTML page with + debug info (using the value `:default`) or render debug info preserving + the response format (using the value `:api`). + + *Jorge Bejar* + +* Fix setting exit status code for rake test tasks. The exit status code + was not set when tests were fired with `rake`. Now, it is being set and it matches + behavior of running tests via `rails` command (`rails test`), so no matter if + `rake test` or `rails test` command is used the exit code will be set. + + *Arkadiusz Fal* + +* Add Command infrastructure to replace rake. + + Also move `rake dev:cache` to new infrastructure. You'll need to use + `rails dev:cache` to toggle development caching from now on. + + *Chuck Callebs* + +* Allow use of minitest-rails gem with Rails test runner. + + Fixes #22455. + + *Chris Kottom* + +* Add `bin/test` script to rails plugin. + + `bin/test` can use the same API as `bin/rails test`. + + *Yuji Yaginuma* + +* Make `static_index` part of the `config.public_file_server` config and + call it `public_file_server.index_name`. + + *Yuki Nishijima* + * Deprecate `serve_static_files` in favor of `public_file_server.enabled`. Unifies the static asset options under `public_file_server`. diff --git a/railties/Rakefile b/railties/Rakefile index 73d881b318..cf130a5f14 100644 --- a/railties/Rakefile +++ b/railties/Rakefile @@ -5,33 +5,20 @@ task :default => :test desc "Run all unit tests" task :test => 'test:isolated' -dash_i = [ - 'test', - 'lib', - "#{File.dirname(__FILE__)}/../activesupport/lib", - "#{File.dirname(__FILE__)}/../actionpack/lib", - "#{File.dirname(__FILE__)}/../activemodel/lib" -] - -dash_i.reverse_each do |x| - $:.unshift x unless $:.include? x -end -$-w = true - -require 'bundler/setup' unless defined?(Bundler) -require 'active_support' - namespace :test do task :isolated do dirs = (ENV["TEST_DIR"] || ENV["TEST_DIRS"] || "**").split(",") test_files = dirs.map { |dir| "test/#{dir}/*_test.rb" } Dir[*test_files].each do |file| next true if file.include?("fixtures") - puts "#{FileUtils::RUBY} -w -I#{dash_i.join ':'} #{file}" - - # We could run these in parallel, but pretty much all of the - # railties tests already run in parallel, so ¯\_(⊙︿⊙)_/¯ - Process.waitpid fork { ARGV.clear; load file } + dash_i = [ + 'test', + 'lib', + "#{File.dirname(__FILE__)}/../activesupport/lib", + "#{File.dirname(__FILE__)}/../actionpack/lib", + "#{File.dirname(__FILE__)}/../activemodel/lib" + ] + ruby "-w", "-I#{dash_i.join ':'}", file end end end diff --git a/railties/lib/rails/application/configuration.rb b/railties/lib/rails/application/configuration.rb index 785671f70b..65cff1561a 100644 --- a/railties/lib/rails/application/configuration.rb +++ b/railties/lib/rails/application/configuration.rb @@ -14,7 +14,7 @@ module Rails :eager_load, :exceptions_app, :file_watcher, :filter_parameters, :force_ssl, :helpers_paths, :logger, :log_formatter, :log_tags, :railties_order, :relative_url_root, :secret_key_base, :secret_token, - :ssl_options, :static_index, :public_file_server, + :ssl_options, :public_file_server, :session_options, :time_zone, :reload_classes_only_on_change, :beginning_of_week, :filter_redirect, :x @@ -24,35 +24,36 @@ module Rails def initialize(*) super self.encoding = "utf-8" - @allow_concurrency = nil - @consider_all_requests_local = false - @filter_parameters = [] - @filter_redirect = [] - @helpers_paths = [] - @static_index = "index" - @public_file_server = ActiveSupport::OrderedOptions.new - @public_file_server.enabled = true - @force_ssl = false - @ssl_options = {} - @session_store = :cookie_store - @session_options = {} - @time_zone = "UTC" - @beginning_of_week = :monday - @log_level = nil - @generators = app_generators - @cache_store = [ :file_store, "#{root}/tmp/cache/" ] - @railties_order = [:all] - @relative_url_root = ENV["RAILS_RELATIVE_URL_ROOT"] - @reload_classes_only_on_change = true - @file_watcher = ActiveSupport::FileUpdateChecker - @exceptions_app = nil - @autoflush_log = true - @log_formatter = ActiveSupport::Logger::SimpleFormatter.new - @eager_load = nil - @secret_token = nil - @secret_key_base = nil - @api_only = false - @x = Custom.new + @allow_concurrency = nil + @consider_all_requests_local = false + @filter_parameters = [] + @filter_redirect = [] + @helpers_paths = [] + @public_file_server = ActiveSupport::OrderedOptions.new + @public_file_server.enabled = true + @public_file_server.index_name = "index" + @force_ssl = false + @ssl_options = {} + @session_store = :cookie_store + @session_options = {} + @time_zone = "UTC" + @beginning_of_week = :monday + @log_level = nil + @generators = app_generators + @cache_store = [ :file_store, "#{root}/tmp/cache/" ] + @railties_order = [:all] + @relative_url_root = ENV["RAILS_RELATIVE_URL_ROOT"] + @reload_classes_only_on_change = true + @file_watcher = ActiveSupport::FileUpdateChecker + @exceptions_app = nil + @autoflush_log = true + @log_formatter = ActiveSupport::Logger::SimpleFormatter.new + @eager_load = nil + @secret_token = nil + @secret_key_base = nil + @api_only = false + @debug_exception_response_format = nil + @x = Custom.new end def static_cache_control=(value) @@ -95,6 +96,16 @@ module Rails def api_only=(value) @api_only = value generators.api_only = value + + @debug_exception_response_format ||= :api + end + + def debug_exception_response_format + @debug_exception_response_format || :default + end + + def debug_exception_response_format=(value) + @debug_exception_response_format = value end def paths @@ -180,22 +191,21 @@ module Rails SourceAnnotationExtractor::Annotation end - private - class Custom #:nodoc: - def initialize - @configurations = Hash.new - end + class Custom #:nodoc: + def initialize + @configurations = Hash.new + end - def method_missing(method, *args) - if method =~ /=$/ - @configurations[$`.to_sym] = args.first - else - @configurations.fetch(method) { - @configurations[method] = ActiveSupport::OrderedOptions.new - } - end + def method_missing(method, *args) + if method =~ /=$/ + @configurations[$`.to_sym] = args.first + else + @configurations.fetch(method) { + @configurations[method] = ActiveSupport::OrderedOptions.new + } end end + end end end end diff --git a/railties/lib/rails/application/default_middleware_stack.rb b/railties/lib/rails/application/default_middleware_stack.rb index b0d481c21a..ed6a1f82d3 100644 --- a/railties/lib/rails/application/default_middleware_stack.rb +++ b/railties/lib/rails/application/default_middleware_stack.rb @@ -21,7 +21,7 @@ module Rails headers = config.public_file_server.headers || {} headers['Cache-Control'.freeze] = config.static_cache_control if config.static_cache_control - middleware.use ::ActionDispatch::Static, paths["public"].first, index: config.static_index, headers: headers + middleware.use ::ActionDispatch::Static, paths["public"].first, index: config.public_file_server.index_name, headers: headers end if rack_cache = load_rack_cache @@ -57,7 +57,7 @@ module Rails # Must come after Rack::MethodOverride to properly log overridden methods middleware.use ::Rails::Rack::Logger, config.log_tags middleware.use ::ActionDispatch::ShowExceptions, show_exceptions_app - middleware.use ::ActionDispatch::DebugExceptions, app + middleware.use ::ActionDispatch::DebugExceptions, app, config.debug_exception_response_format middleware.use ::ActionDispatch::RemoteIp, config.action_dispatch.ip_spoofing_check, config.action_dispatch.trusted_proxies unless config.cache_classes diff --git a/railties/lib/rails/command.rb b/railties/lib/rails/command.rb new file mode 100644 index 0000000000..f7753cbb83 --- /dev/null +++ b/railties/lib/rails/command.rb @@ -0,0 +1,70 @@ +require 'rails/commands/commands_tasks' + +module Rails + class Command #:nodoc: + attr_reader :argv + + def initialize(argv = []) + @argv = argv + + @option_parser = build_option_parser + @options = {} + end + + def self.run(task_name, argv) + command_name = command_name_for(task_name) + + if command = command_for(command_name) + command.new(argv).run(command_name) + else + Rails::CommandsTasks.new(argv).run_command!(task_name) + end + end + + def run(command_name) + parse_options_for(command_name) + @option_parser.parse! @argv + + public_send(command_name) + end + + def self.options_for(command_name, &options_to_parse) + @@command_options[command_name] = options_to_parse + end + + def self.set_banner(command_name, banner) + options_for(command_name) { |opts, _| opts.banner = banner } + end + + private + @@commands = [] + @@command_options = {} + + def parse_options_for(command_name) + @@command_options.fetch(command_name, proc {}).call(@option_parser, @options) + end + + def build_option_parser + OptionParser.new do |opts| + opts.on('-h', '--help', 'Show this help.') do + puts opts + exit + end + end + end + + def self.inherited(command) + @@commands << command + end + + def self.command_name_for(task_name) + task_name.gsub(':', '_').to_sym + end + + def self.command_for(command_name) + @@commands.find do |command| + command.public_instance_methods.include?(command_name) + end + end + end +end diff --git a/railties/lib/rails/commands.rb b/railties/lib/rails/commands.rb index 12bd73db24..7627fcf5a0 100644 --- a/railties/lib/rails/commands.rb +++ b/railties/lib/rails/commands.rb @@ -7,12 +7,13 @@ aliases = { "s" => "server", "db" => "dbconsole", "r" => "runner", - "t" => "test", + "t" => "test" } command = ARGV.shift command = aliases[command] || command -require 'rails/commands/commands_tasks' +require 'rails/command' +require 'rails/commands/dev_cache' -Rails::CommandsTasks.new(ARGV).run_command!(command) +Rails::Command.run(command, ARGV) diff --git a/railties/lib/rails/commands/commands_tasks.rb b/railties/lib/rails/commands/commands_tasks.rb index 685d55eea8..da3b9452d5 100644 --- a/railties/lib/rails/commands/commands_tasks.rb +++ b/railties/lib/rails/commands/commands_tasks.rb @@ -1,3 +1,5 @@ +require 'rails/commands/rake_proxy' + module Rails # This is a class which takes in a rails command and initiates the appropriate # initiation sequence. @@ -5,6 +7,8 @@ module Rails # Warning: This class mutates ARGV because some commands require manipulating # it before they are run. class CommandsTasks # :nodoc: + include Rails::RakeProxy + attr_reader :argv HELP_MESSAGE = <<-EOT @@ -20,14 +24,18 @@ The most common rails commands are: new Create a new Rails application. "rails new my_app" creates a new application called MyApp in "./my_app" -In addition to those, there are: - destroy Undo code generated with "generate" (short-cut alias: "d") - plugin new Generates skeleton for developing a Rails plugin - runner Run a piece of code in the application environment (short-cut alias: "r") - All commands can be run with -h (or --help) for more information. + +In addition to those commands, there are: EOT + ADDITIONAL_COMMANDS = [ + [ 'destroy', 'Undo code generated with "generate" (short-cut alias: "d")' ], + [ 'plugin new', 'Generates skeleton for developing a Rails plugin' ], + [ 'runner', + 'Run a piece of code in the application environment (short-cut alias: "r")' ] + ] + COMMAND_WHITELIST = %w(plugin generate destroy console server dbconsole runner new version help test) def initialize(argv) @@ -36,10 +44,11 @@ EOT def run_command!(command) command = parse_command(command) + if COMMAND_WHITELIST.include?(command) send(command) else - write_error_message(command) + run_rake_task(command) end end @@ -110,6 +119,7 @@ EOT def help write_help_message + write_commands ADDITIONAL_COMMANDS + formatted_rake_tasks end private @@ -151,24 +161,9 @@ 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? - puts "Did you mean: `$ rake #{command}` ?\n\n" - end - write_help_message - exit(1) + def write_commands(commands) + width = commands.map { |name, _| name.size }.max || 10 + commands.each { |command| printf(" %-#{width}s %s\n", *command) } end def parse_command(command) diff --git a/railties/lib/rails/commands/dbconsole.rb b/railties/lib/rails/commands/dbconsole.rb index dca60f948f..2c36edfa3f 100644 --- a/railties/lib/rails/commands/dbconsole.rb +++ b/railties/lib/rails/commands/dbconsole.rb @@ -65,7 +65,7 @@ module Rails 'sslca' => '--ssl-ca', 'sslcert' => '--ssl-cert', 'sslcapath' => '--ssl-capath', - 'sslcipher' => '--ssh-cipher', + 'sslcipher' => '--ssl-cipher', 'sslkey' => '--ssl-key' }.map { |opt, arg| "#{arg}=#{config[opt]}" if config[opt] }.compact diff --git a/railties/lib/rails/commands/dev_cache.rb b/railties/lib/rails/commands/dev_cache.rb new file mode 100644 index 0000000000..ec96e8f630 --- /dev/null +++ b/railties/lib/rails/commands/dev_cache.rb @@ -0,0 +1,21 @@ +require 'rails/command' + +module Rails + module Commands + # This is a wrapper around the Rails dev:cache command + class DevCache < Command + set_banner :dev_cache, 'Toggle development mode caching on/off' + def dev_cache + if File.exist? 'tmp/caching-dev.txt' + File.delete 'tmp/caching-dev.txt' + puts 'Development mode is no longer being cached.' + else + FileUtils.touch 'tmp/caching-dev.txt' + puts 'Development mode is now being cached.' + end + + FileUtils.touch 'tmp/restart.txt' + end + end + end +end diff --git a/railties/lib/rails/commands/rake_proxy.rb b/railties/lib/rails/commands/rake_proxy.rb new file mode 100644 index 0000000000..f7d5df6b2f --- /dev/null +++ b/railties/lib/rails/commands/rake_proxy.rb @@ -0,0 +1,34 @@ +require 'rake' +require 'active_support' + +module Rails + module RakeProxy #:nodoc: + private + def run_rake_task(command) + ARGV.unshift(command) # Prepend the command, so Rake knows how to run it. + + Rake.application.standard_exception_handling do + Rake.application.init('rails') + Rake.application.load_rakefile + Rake.application.top_level + end + end + + def rake_tasks + return @rake_tasks if defined?(@rake_tasks) + + ActiveSupport::Deprecation.silence do + require_application_and_environment! + end + + Rake::TaskManager.record_task_metadata = true + Rake.application.instance_variable_set(:@name, 'rails') + Rails.application.load_tasks + @rake_tasks = Rake.application.tasks.select(&:comment) + end + + def formatted_rake_tasks + rake_tasks.map { |t| [ t.name_with_args, t.comment ] } + end + end +end diff --git a/railties/lib/rails/generators.rb b/railties/lib/rails/generators.rb index 2645102619..e3d79521e7 100644 --- a/railties/lib/rails/generators.rb +++ b/railties/lib/rails/generators.rb @@ -208,6 +208,7 @@ module Rails "#{test}:model", "#{test}:scaffold", "#{test}:view", + "#{test}:job", "#{template}:controller", "#{template}:scaffold", "#{template}:mailer", 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 d99b27cb2d..519b6c8603 100644 --- a/railties/lib/rails/generators/erb/scaffold/templates/_form.html.erb +++ b/railties/lib/rails/generators/erb/scaffold/templates/_form.html.erb @@ -14,15 +14,15 @@ <% attributes.each do |attribute| -%> <div class="field"> <% if attribute.password_digest? -%> - <%%= f.label :password %><br> + <%%= f.label :password %> <%%= f.password_field :password %> </div> <div class="field"> - <%%= f.label :password_confirmation %><br> + <%%= f.label :password_confirmation %> <%%= f.password_field :password_confirmation %> <% else -%> - <%%= f.label :<%= attribute.column_name %> %><br> + <%%= f.label :<%= attribute.column_name %> %> <%%= f.<%= attribute.field_type %> :<%= attribute.column_name %> %> <% end -%> </div> diff --git a/railties/lib/rails/generators/rails/app/app_generator.rb b/railties/lib/rails/generators/rails/app/app_generator.rb index b813b083f4..889154494d 100644 --- a/railties/lib/rails/generators/rails/app/app_generator.rb +++ b/railties/lib/rails/generators/rails/app/app_generator.rb @@ -303,6 +303,7 @@ module Rails if options[:api] remove_file 'config/initializers/session_store.rb' remove_file 'config/initializers/cookies_serializer.rb' + remove_file 'config/initializers/request_forgery_protection.rb' end end diff --git a/railties/lib/rails/generators/rails/app/templates/Gemfile b/railties/lib/rails/generators/rails/app/templates/Gemfile index 975be07622..2f7ffc055d 100644 --- a/railties/lib/rails/generators/rails/app/templates/Gemfile +++ b/railties/lib/rails/generators/rails/app/templates/Gemfile @@ -41,7 +41,7 @@ group :development do <%- if options.dev? || options.edge? -%> gem 'web-console', github: 'rails/web-console' <%- else -%> - gem 'web-console', '~> 2.0' + gem 'web-console', '~> 3.0' <%- end -%> <%- end -%> <% if spring_install? -%> diff --git a/railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt index 4dd20a9d2e..65c6aeb694 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt +++ b/railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt @@ -23,7 +23,6 @@ Rails.application.configure do config.action_controller.perform_caching = false config.cache_store = :null_store end - <%- unless options.skip_action_mailer? -%> # Don't care if the mailer can't send. @@ -56,4 +55,8 @@ Rails.application.configure do # Raises error for missing translations # config.action_view.raise_on_missing_translations = true + + # Use an evented file watcher to asynchronously detect changes in source code, + # routes, locales, etc. This feature depends on the listen gem. + # config.file_watcher = ActiveSupport::EventedFileUpdateChecker end diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/request_forgery_protection.rb b/railties/lib/rails/generators/rails/app/templates/config/initializers/request_forgery_protection.rb new file mode 100644 index 0000000000..3eab78a885 --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/request_forgery_protection.rb @@ -0,0 +1,4 @@ +# Be sure to restart your server when you modify this file. + +# Enable origin-checking CSRF mitigation. +Rails.application.config.action_controller.forgery_protection_origin_check = true diff --git a/railties/lib/rails/generators/rails/plugin/plugin_generator.rb b/railties/lib/rails/generators/rails/plugin/plugin_generator.rb index eeeef430bb..776019a6a0 100644 --- a/railties/lib/rails/generators/rails/plugin/plugin_generator.rb +++ b/railties/lib/rails/generators/rails/plugin/plugin_generator.rb @@ -148,9 +148,8 @@ task default: :test end def bin(force = false) - return unless engine? - - directory "bin", force: force do |content| + bin_file = engine? ? 'bin/rails.tt' : 'bin/test.tt' + template bin_file, force: force do |content| "#{shebang}\n" + content end chmod "bin", 0755, verbose: false diff --git a/railties/lib/rails/generators/rails/plugin/templates/%name%.gemspec b/railties/lib/rails/generators/rails/plugin/templates/%name%.gemspec index ff242adbab..3f5682b0c1 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/%name%.gemspec +++ b/railties/lib/rails/generators/rails/plugin/templates/%name%.gemspec @@ -15,9 +15,6 @@ Gem::Specification.new do |s| s.license = "MIT" s.files = Dir["{app,config,db,lib}/**/*", "MIT-LICENSE", "Rakefile", "README.rdoc"] -<% unless options.skip_test? -%> - s.test_files = Dir["test/**/*"] -<% end -%> <%= '# ' if options.dev? || options.edge? -%>s.add_dependency "rails", "~> <%= Rails::VERSION::STRING %>" <% unless options[:skip_active_record] -%> diff --git a/railties/lib/rails/generators/rails/plugin/templates/bin/test.tt b/railties/lib/rails/generators/rails/plugin/templates/bin/test.tt new file mode 100644 index 0000000000..62b94618fd --- /dev/null +++ b/railties/lib/rails/generators/rails/plugin/templates/bin/test.tt @@ -0,0 +1,8 @@ +$: << File.expand_path(File.expand_path('../../test', __FILE__)) + +require 'bundler/setup' +require 'rails/test_unit/minitest_plugin' + +Rails::TestUnitReporter.executable = 'bin/test' + +exit Minitest.run(ARGV) diff --git a/railties/lib/rails/generators/rails/plugin/templates/rails/routes.rb b/railties/lib/rails/generators/rails/plugin/templates/rails/routes.rb index 673de44108..694510edc0 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/rails/routes.rb +++ b/railties/lib/rails/generators/rails/plugin/templates/rails/routes.rb @@ -1,4 +1,3 @@ Rails.application.routes.draw do - mount <%= camelized_modules %>::Engine => "/<%= name %>" end diff --git a/railties/lib/rails/generators/rails/plugin/templates/test/test_helper.rb b/railties/lib/rails/generators/rails/plugin/templates/test/test_helper.rb index f315144723..a0b00fc5c5 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/test/test_helper.rb +++ b/railties/lib/rails/generators/rails/plugin/templates/test/test_helper.rb @@ -14,6 +14,10 @@ require "rails/test_help" # to be shown. Minitest.backtrace_filter = Minitest::BacktraceFilter.new +<% unless engine? -%> +Rails::TestUnitReporter.executable = 'bin/test' +<% end -%> + # Load fixtures from the engine if ActiveSupport::TestCase.respond_to?(:fixture_path=) ActiveSupport::TestCase.fixture_path = File.expand_path("../fixtures", __FILE__) diff --git a/railties/lib/rails/generators/rails/scaffold/templates/scaffold.css b/railties/lib/rails/generators/rails/scaffold/templates/scaffold.css index b7818883d1..79f8b7f96f 100644 --- a/railties/lib/rails/generators/rails/scaffold/templates/scaffold.css +++ b/railties/lib/rails/generators/rails/scaffold/templates/scaffold.css @@ -78,3 +78,7 @@ div.actions { font-size: 12px; list-style: square; } + +label { + display: block; +} diff --git a/railties/lib/rails/generators/rails/scaffold_controller/templates/api_controller.rb b/railties/lib/rails/generators/rails/scaffold_controller/templates/api_controller.rb index bc3c9b3f6b..400afec6dc 100644 --- a/railties/lib/rails/generators/rails/scaffold_controller/templates/api_controller.rb +++ b/railties/lib/rails/generators/rails/scaffold_controller/templates/api_controller.rb @@ -1,5 +1,5 @@ <% if namespaced? -%> -require_dependency "<%= namespaced_file_path %>/application_controller" +require_dependency "<%= namespaced_path %>/application_controller" <% end -%> <% module_namespacing do -%> @@ -52,7 +52,7 @@ class <%= controller_class_name %>Controller < ApplicationController # Only allow a trusted parameter "white list" through. def <%= "#{singular_table_name}_params" %> <%- if attributes_names.empty? -%> - params[:<%= singular_table_name %>] + params.fetch(:<%= singular_table_name %>, {}) <%- else -%> params.require(:<%= singular_table_name %>).permit(<%= attributes_names.map { |name| ":#{name}" }.join(', ') %>) <%- end -%> diff --git a/railties/lib/rails/generators/rails/scaffold_controller/templates/controller.rb b/railties/lib/rails/generators/rails/scaffold_controller/templates/controller.rb index f73e9a96ba..42b9e34274 100644 --- a/railties/lib/rails/generators/rails/scaffold_controller/templates/controller.rb +++ b/railties/lib/rails/generators/rails/scaffold_controller/templates/controller.rb @@ -59,7 +59,7 @@ class <%= controller_class_name %>Controller < ApplicationController # Only allow a trusted parameter "white list" through. def <%= "#{singular_table_name}_params" %> <%- if attributes_names.empty? -%> - params[:<%= singular_table_name %>] + params.fetch(:<%= singular_table_name %>, {}) <%- else -%> params.require(:<%= singular_table_name %>).permit(<%= attributes_names.map { |name| ":#{name}" }.join(', ') %>) <%- end -%> diff --git a/railties/lib/rails/tasks.rb b/railties/lib/rails/tasks.rb index d3e33584d7..d60eaf6f4f 100644 --- a/railties/lib/rails/tasks.rb +++ b/railties/lib/rails/tasks.rb @@ -3,7 +3,6 @@ require 'rake' # Load Rails Rakefile extensions %w( annotations - dev framework initializers log diff --git a/railties/lib/rails/tasks/dev.rake b/railties/lib/rails/tasks/dev.rake deleted file mode 100644 index e949172d3f..0000000000 --- a/railties/lib/rails/tasks/dev.rake +++ /dev/null @@ -1,15 +0,0 @@ -namespace :dev do - task :cache do - desc 'Toggle development mode caching on/off' - - if File.exist? 'tmp/caching-dev.txt' - File.delete 'tmp/caching-dev.txt' - puts 'Development mode is no longer being cached.' - else - FileUtils.touch 'tmp/caching-dev.txt' - puts 'Development mode is now being cached.' - end - - FileUtils.touch 'tmp/restart.txt' - end -end diff --git a/railties/lib/rails/test_unit/minitest_plugin.rb b/railties/lib/rails/test_unit/minitest_plugin.rb index d1ba35a5ec..d39d2f32bf 100644 --- a/railties/lib/rails/test_unit/minitest_plugin.rb +++ b/railties/lib/rails/test_unit/minitest_plugin.rb @@ -14,15 +14,16 @@ module Minitest SummaryReporter.prepend AggregatedResultSuppresion def self.plugin_rails_options(opts, options) + executable = ::Rails::TestUnitReporter.executable opts.separator "" - opts.separator "Usage: bin/rails test [options] [files or directories]" + opts.separator "Usage: #{executable} [options] [files or directories]" opts.separator "You can run a single test by appending a line number to a filename:" opts.separator "" - opts.separator " bin/rails test test/models/user_test.rb:27" + opts.separator " #{executable} test/models/user_test.rb:27" opts.separator "" opts.separator "You can run multiple files and directories at the same time:" opts.separator "" - opts.separator " bin/rails test test/controllers test/integration/login_test.rb" + opts.separator " #{executable} test/controllers test/integration/login_test.rb" opts.separator "" opts.separator "By default test failures and errors are reported inline during a run." opts.separator "" @@ -56,7 +57,9 @@ module Minitest # as the patterns would also contain the other Rake tasks. def self.rake_run(patterns) # :nodoc: @rake_patterns = patterns - run + passed = run + exit passed unless passed + passed end def self.plugin_rails_init(options) diff --git a/railties/lib/rails/test_unit/reporter.rb b/railties/lib/rails/test_unit/reporter.rb index e1fe92a11b..00ea32d1b8 100644 --- a/railties/lib/rails/test_unit/reporter.rb +++ b/railties/lib/rails/test_unit/reporter.rb @@ -44,7 +44,7 @@ module Rails end def relative_path_for(file) - file.sub(/^#{Rails.root}\/?/, '') + file.sub(/^#{app_root}\/?/, '') end private @@ -57,8 +57,18 @@ module Rails end def format_rerun_snippet(result) - location, line = result.method(result.name).source_location - "#{self.executable} #{relative_path_for(location)}:#{line}" + # Try to extract path to assertion from backtrace. + if result.location =~ /\[(.*)\]\z/ + assertion_path = $1 + else + assertion_path = result.method(result.name).source_location.join(':') + end + + "#{self.executable} #{relative_path_for(assertion_path)}" + end + + def app_root + @app_root ||= defined?(ENGINE_ROOT) ? ENGINE_ROOT : Rails.root end end end diff --git a/railties/test/application/configuration_test.rb b/railties/test/application/configuration_test.rb index 5f3d1879eb..49f63d5d31 100644 --- a/railties/test/application/configuration_test.rb +++ b/railties/test/application/configuration_test.rb @@ -1393,5 +1393,45 @@ module ApplicationTests assert_equal 'unicorn', Rails.application.config.my_custom_config['key'] end + + test "api_only is false by default" do + app 'development' + refute Rails.application.config.api_only + end + + test "api_only generator config is set when api_only is set" do + add_to_config <<-RUBY + config.api_only = true + RUBY + app 'development' + + Rails.application.load_generators + assert Rails.configuration.api_only + end + + test "debug_exception_response_format is :api by default if only_api is enabled" do + add_to_config <<-RUBY + config.api_only = true + RUBY + app 'development' + + assert_equal :api, Rails.configuration.debug_exception_response_format + end + + test "debug_exception_response_format can be override" do + add_to_config <<-RUBY + config.api_only = true + RUBY + + app_file 'config/environments/development.rb', <<-RUBY + Rails.application.configure do + config.debug_exception_response_format = :default + end + RUBY + + app 'development' + + assert_equal :default, Rails.configuration.debug_exception_response_format + end end end diff --git a/railties/test/application/loading_test.rb b/railties/test/application/loading_test.rb index 1027bca2c1..2106708c98 100644 --- a/railties/test/application/loading_test.rb +++ b/railties/test/application/loading_test.rb @@ -169,6 +169,8 @@ class LoadingTest < ActiveSupport::TestCase config.file_watcher = Class.new do def initialize(*); end def updated?; false; end + def execute; end + def execute_if_updated; false; end end RUBY diff --git a/railties/test/application/middleware/static_test.rb b/railties/test/application/middleware/static_test.rb index 5366537dc2..1246e20d94 100644 --- a/railties/test/application/middleware/static_test.rb +++ b/railties/test/application/middleware/static_test.rb @@ -44,7 +44,7 @@ module ApplicationTests assert_equal 'public, max-age=60', last_response.headers["Cache-Control"] end - test "static_index defaults to 'index'" do + test "public_file_server.index_name defaults to 'index'" do app_file "public/index.html", "/index.html" require "#{app_path}/config/environment" @@ -54,10 +54,10 @@ module ApplicationTests assert_equal "/index.html\n", last_response.body end - test "static_index configurable" do + test "public_file_server.index_name configurable" do app_file "public/other-index.html", "/other-index.html" - add_to_config "config.static_index = 'other-index'" - + add_to_config "config.public_file_server.index_name = 'other-index'" + require "#{app_path}/config/environment" get '/' diff --git a/railties/test/application/rake/dbs_test.rb b/railties/test/application/rake/dbs_test.rb index f94d08673a..0b0fb50fe1 100644 --- a/railties/test/application/rake/dbs_test.rb +++ b/railties/test/application/rake/dbs_test.rb @@ -61,7 +61,7 @@ module ApplicationTests test 'db:create failure because database exists' do with_database_existing do output = `bin/rake db:create 2>&1` - assert_match /already exists/, output + assert_match(/already exists/, output) assert_equal 0, $?.exitstatus end end @@ -78,7 +78,7 @@ module ApplicationTests test 'db:create failure because bad permissions' do with_bad_permissions do output = `bin/rake db:create 2>&1` - assert_match /Couldn't create database/, output + assert_match(/Couldn't create database/, output) assert_equal 1, $?.exitstatus end end @@ -86,7 +86,7 @@ module ApplicationTests test 'db:drop failure because database does not exist' do Dir.chdir(app_path) do output = `bin/rake db:drop 2>&1` - assert_match /does not exist/, output + assert_match(/does not exist/, output) assert_equal 0, $?.exitstatus end end @@ -95,7 +95,7 @@ module ApplicationTests with_database_existing do with_bad_permissions do output = `bin/rake db:drop 2>&1` - assert_match /Couldn't drop/, output + assert_match(/Couldn't drop/, output) assert_equal 1, $?.exitstatus end end diff --git a/railties/test/application/rake/dev_test.rb b/railties/test/application/rake/dev_test.rb deleted file mode 100644 index 28d8b22a37..0000000000 --- a/railties/test/application/rake/dev_test.rb +++ /dev/null @@ -1,35 +0,0 @@ -require 'isolation/abstract_unit' - -module ApplicationTests - module RakeTests - class RakeDevTest < ActiveSupport::TestCase - include ActiveSupport::Testing::Isolation - - def setup - build_app - boot_rails - end - - def teardown - teardown_app - end - - test 'dev:cache creates file and outputs message' do - Dir.chdir(app_path) do - output = `rake dev:cache` - assert File.exist?('tmp/caching-dev.txt') - assert_match(/Development mode is now being cached/, output) - end - end - - test 'dev:cache deletes file and outputs message' do - Dir.chdir(app_path) do - output = `rake dev:cache` - output = `rake dev:cache` - assert_not File.exist?('tmp/caching-dev.txt') - assert_match(/Development mode is no longer being cached/, output) - end - end - end - end -end diff --git a/railties/test/application/test_runner_test.rb b/railties/test/application/test_runner_test.rb index 0aa6ce2252..4965ab7da0 100644 --- a/railties/test/application/test_runner_test.rb +++ b/railties/test/application/test_runner_test.rb @@ -344,7 +344,7 @@ module ApplicationTests create_test_file :models, 'post', pass: false output = run_test_command('test/models/post_test.rb') - assert_match %r{Running:\n\nPostTest\nF\n\nwups!\n\nbin/rails test test/models/post_test.rb:4}, output + assert_match %r{Running:\n\nPostTest\nF\n\nwups!\n\nbin/rails test test/models/post_test.rb:6}, output end def test_only_inline_failure_output diff --git a/railties/test/commands/dev_cache_test.rb b/railties/test/commands/dev_cache_test.rb new file mode 100644 index 0000000000..1b7a72e7fc --- /dev/null +++ b/railties/test/commands/dev_cache_test.rb @@ -0,0 +1,32 @@ +require_relative '../isolation/abstract_unit' + +module CommandsTests + class DevCacheTest < ActiveSupport::TestCase + include ActiveSupport::Testing::Isolation + + def setup + build_app + end + + def teardown + teardown_app + end + + test 'dev:cache creates file and outputs message' do + Dir.chdir(app_path) do + output = `rails dev:cache` + assert File.exist?('tmp/caching-dev.txt') + assert_match(%r{Development mode is now being cached}, output) + end + end + + test 'dev:cache deletes file and outputs message' do + Dir.chdir(app_path) do + `rails dev:cache` # Create caching file. + output = `rails dev:cache` # Delete caching file. + assert_not File.exist?('tmp/caching-dev.txt') + assert_match(%r{Development mode is no longer being cached}, output) + end + end + end +end diff --git a/railties/test/generators/api_app_generator_test.rb b/railties/test/generators/api_app_generator_test.rb index 998da3ef84..be2cd3a853 100644 --- a/railties/test/generators/api_app_generator_test.rb +++ b/railties/test/generators/api_app_generator_test.rb @@ -89,6 +89,7 @@ class ApiAppGeneratorTest < Rails::Generators::TestCase config/initializers/assets.rb config/initializers/cookies_serializer.rb config/initializers/session_store.rb + config/initializers/request_forgery_protection.rb lib/assets vendor/assets test/helpers diff --git a/railties/test/generators/app_generator_test.rb b/railties/test/generators/app_generator_test.rb index 446fef562b..dd2e931c5c 100644 --- a/railties/test/generators/app_generator_test.rb +++ b/railties/test/generators/app_generator_test.rb @@ -505,7 +505,7 @@ class AppGeneratorTest < Rails::Generators::TestCase assert_file "Gemfile" do |content| assert_match(/gem 'web-console',\s+github: 'rails\/web-console'/, content) - assert_no_match(/gem 'web-console', '~> 2.0'/, content) + assert_no_match(/gem 'web-console', '~> 3.0'/, content) end end @@ -514,7 +514,7 @@ class AppGeneratorTest < Rails::Generators::TestCase assert_file "Gemfile" do |content| assert_match(/gem 'web-console',\s+github: 'rails\/web-console'/, content) - assert_no_match(/gem 'web-console', '~> 2.0'/, content) + assert_no_match(/gem 'web-console', '~> 3.0'/, content) end end diff --git a/railties/test/generators/namespaced_generators_test.rb b/railties/test/generators/namespaced_generators_test.rb index c4ee6602c5..590f06e19a 100644 --- a/railties/test/generators/namespaced_generators_test.rb +++ b/railties/test/generators/namespaced_generators_test.rb @@ -396,4 +396,28 @@ class NamespacedScaffoldGeneratorTest < NamespacedGeneratorTestCase # Stylesheets (should not be removed) assert_file "app/assets/stylesheets/scaffold.css" end + + def test_api_scaffold_with_namespace_on_invoke + run_generator [ "admin/role", "name:string", "description:string", "--api" ] + + # Model + assert_file "app/models/test_app/admin.rb", /module TestApp\n module Admin/ + assert_file "app/models/test_app/admin/role.rb", /module TestApp\n class Admin::Role < ActiveRecord::Base/ + assert_file "test/models/test_app/admin/role_test.rb", /module TestApp\n class Admin::RoleTest < ActiveSupport::TestCase/ + assert_file "test/fixtures/test_app/admin/roles.yml" + assert_migration "db/migrate/create_test_app_admin_roles.rb" + + # Route + assert_file "config/routes.rb" do |route| + assert_match(/^ namespace :admin do\n resources :roles\n end$/, route) + end + + # Controller + assert_file "app/controllers/test_app/admin/roles_controller.rb" do |content| + assert_match(/module TestApp\n class Admin::RolesController < ApplicationController/, content) + assert_match(%r(require_dependency "test_app/application_controller"), content) + end + assert_file "test/controllers/test_app/admin/roles_controller_test.rb", + /module TestApp\n class Admin::RolesControllerTest < ActionController::TestCase/ + end end diff --git a/railties/test/generators/plugin_generator_test.rb b/railties/test/generators/plugin_generator_test.rb index 715debf344..60390cfa01 100644 --- a/railties/test/generators/plugin_generator_test.rb +++ b/railties/test/generators/plugin_generator_test.rb @@ -65,8 +65,11 @@ class PluginGeneratorTest < Rails::Generators::TestCase assert_match(/require.+test\/dummy\/config\/environment/, content) assert_match(/ActiveRecord::Migrator\.migrations_paths.+test\/dummy\/db\/migrate/, content) assert_match(/Minitest\.backtrace_filter = Minitest::BacktraceFilter\.new/, content) + assert_match(/Rails::TestUnitReporter\.executable = 'bin\/test'/, content) end assert_file "test/bukkits_test.rb", /assert_kind_of Module, Bukkits/ + assert_file 'bin/test' + assert_no_file 'bin/rails' end def test_generating_test_files_in_full_mode @@ -223,7 +226,7 @@ class PluginGeneratorTest < Rails::Generators::TestCase run_generator FileUtils.cd destination_root quietly { system 'bundle install' } - assert_match(/1 runs, 1 assertions, 0 failures, 0 errors/, `bundle exec rake test 2>&1`) + assert_match(/1 runs, 1 assertions, 0 failures, 0 errors/, `bin/test 2>&1`) end def test_ensure_that_tests_works_in_full_mode @@ -315,7 +318,9 @@ class PluginGeneratorTest < Rails::Generators::TestCase assert_match(/ActiveRecord::Migrator\.migrations_paths.+\.\.\/test\/dummy\/db\/migrate/, content) assert_match(/ActiveRecord::Migrator\.migrations_paths.+<<.+\.\.\/db\/migrate/, content) assert_match(/ActionDispatch::IntegrationTest\.fixture_path = ActiveSupport::TestCase\.fixture_pat/, content) + assert_no_match(/Rails::TestUnitReporter\.executable = 'bin\/test'/, content) end + assert_no_file 'bin/test' end def test_create_mountable_application_with_mountable_option_and_hypenated_name @@ -382,7 +387,6 @@ class PluginGeneratorTest < Rails::Generators::TestCase run_generator assert_file "bukkits.gemspec", /s.name\s+= "bukkits"/ assert_file "bukkits.gemspec", /s.files = Dir\["\{app,config,db,lib\}\/\*\*\/\*", "MIT-LICENSE", "Rakefile", "README\.rdoc"\]/ - assert_file "bukkits.gemspec", /s.test_files = Dir\["test\/\*\*\/\*"\]/ assert_file "bukkits.gemspec", /s.version\s+ = Bukkits::VERSION/ end @@ -456,9 +460,6 @@ class PluginGeneratorTest < Rails::Generators::TestCase def test_skipping_test_files run_generator [destination_root, "--skip-test"] assert_no_file "test" - assert_file "bukkits.gemspec" do |contents| - assert_no_match(/s.test_files = Dir\["test\/\*\*\/\*"\]/, contents) - end assert_file '.gitignore' do |contents| assert_no_match(/test\dummy/, contents) end diff --git a/railties/test/generators/plugin_test_helper.rb b/railties/test/generators/plugin_test_helper.rb new file mode 100644 index 0000000000..96c1b1d31f --- /dev/null +++ b/railties/test/generators/plugin_test_helper.rb @@ -0,0 +1,24 @@ +require 'abstract_unit' +require 'tmpdir' + +module PluginTestHelper + def create_test_file(name, pass: true) + plugin_file "test/#{name}_test.rb", <<-RUBY + require 'test_helper' + + class #{name.camelize}Test < ActiveSupport::TestCase + def test_truth + puts "#{name.camelize}Test" + assert #{pass}, 'wups!' + end + end + RUBY + end + + def plugin_file(path, contents, mode: 'w') + FileUtils.mkdir_p File.dirname("#{plugin_path}/#{path}") + File.open("#{plugin_path}/#{path}", mode) do |f| + f.puts contents + end + end +end diff --git a/railties/test/generators/plugin_test_runner_test.rb b/railties/test/generators/plugin_test_runner_test.rb new file mode 100644 index 0000000000..716728819e --- /dev/null +++ b/railties/test/generators/plugin_test_runner_test.rb @@ -0,0 +1,104 @@ +require 'generators/plugin_test_helper' + +class PluginTestRunnerTest < ActiveSupport::TestCase + include PluginTestHelper + + def setup + @destination_root = Dir.mktmpdir('bukkits') + Dir.chdir(@destination_root) { `bundle exec rails plugin new bukkits --skip-bundle` } + plugin_file 'test/dummy/db/schema.rb', '' + end + + def teardown + FileUtils.rm_rf(@destination_root) + end + + def test_run_single_file + create_test_file 'foo' + create_test_file 'bar' + assert_match "1 runs, 1 assertions, 0 failures", run_test_command("test/foo_test.rb") + end + + def test_run_multiple_files + create_test_file 'foo' + create_test_file 'bar' + assert_match "2 runs, 2 assertions, 0 failures", run_test_command("test/foo_test.rb test/bar_test.rb") + end + + def test_mix_files_and_line_filters + create_test_file 'account' + plugin_file 'test/post_test.rb', <<-RUBY + require 'test_helper' + + class PostTest < ActiveSupport::TestCase + def test_post + puts 'PostTest' + assert true + end + + def test_line_filter_does_not_run_this + assert true + end + end + RUBY + + run_test_command('test/account_test.rb test/post_test.rb:4').tap do |output| + assert_match 'AccountTest', output + assert_match 'PostTest', output + assert_match '2 runs, 2 assertions', output + end + end + + def test_multiple_line_filters + create_test_file 'account' + create_test_file 'post' + + run_test_command('test/account_test.rb:4 test/post_test.rb:4').tap do |output| + assert_match 'AccountTest', output + assert_match 'PostTest', output + end + end + + def test_line_filter_without_line_runs_all_tests + create_test_file 'account' + + run_test_command('test/account_test.rb:').tap do |output| + assert_match 'AccountTest', output + end + end + + def test_output_inline_by_default + create_test_file 'post', pass: false + + output = run_test_command('test/post_test.rb') + assert_match %r{Running:\n\nPostTest\nF\n\nwups!\n\nbin/test (/private)?#{plugin_path}/test/post_test.rb:6}, output + end + + def test_only_inline_failure_output + create_test_file 'post', pass: false + + output = run_test_command('test/post_test.rb') + assert_match %r{Finished in.*\n\n1 runs, 1 assertions}, output + end + + def test_fail_fast + create_test_file 'post', pass: false + + assert_match(/Interrupt/, + capture(:stderr) { run_test_command('test/post_test.rb --fail-fast') }) + end + + def test_raise_error_when_specified_file_does_not_exist + error = capture(:stderr) { run_test_command('test/not_exists.rb') } + assert_match(%r{cannot load such file.+test/not_exists\.rb}, error) + end + + private + def plugin_path + "#{@destination_root}/bukkits" + end + + def run_test_command(arguments) + Dir.chdir(plugin_path) { `bin/test #{arguments}` } + end +end diff --git a/railties/test/generators/scaffold_controller_generator_test.rb b/railties/test/generators/scaffold_controller_generator_test.rb index 95ef853a11..b4ba101017 100644 --- a/railties/test/generators/scaffold_controller_generator_test.rb +++ b/railties/test/generators/scaffold_controller_generator_test.rb @@ -56,7 +56,7 @@ class ScaffoldControllerGeneratorTest < Rails::Generators::TestCase assert_file "app/controllers/users_controller.rb" do |content| assert_match(/def user_params/, content) - assert_match(/params\[:user\]/, content) + assert_match(/params\.fetch\(:user, \{\}\)/, content) end end diff --git a/railties/test/generators/test_runner_in_engine_test.rb b/railties/test/generators/test_runner_in_engine_test.rb new file mode 100644 index 0000000000..641c5d0835 --- /dev/null +++ b/railties/test/generators/test_runner_in_engine_test.rb @@ -0,0 +1,31 @@ +require 'generators/plugin_test_helper' + +class TestRunnerInEngineTest < ActiveSupport::TestCase + include PluginTestHelper + + def setup + @destination_root = Dir.mktmpdir('bukkits') + Dir.chdir(@destination_root) { `bundle exec rails plugin new bukkits --full --skip-bundle` } + plugin_file 'test/dummy/db/schema.rb', '' + end + + def teardown + FileUtils.rm_rf(@destination_root) + end + + def test_rerun_snippet_is_relative_path + create_test_file 'post', pass: false + + output = run_test_command('test/post_test.rb') + assert_match %r{Running:\n\nPostTest\nF\n\nwups!\n\nbin/rails test test/post_test.rb:6}, output + end + + private + def plugin_path + "#{@destination_root}/bukkits" + end + + def run_test_command(arguments) + Dir.chdir(plugin_path) { `bin/rails test #{arguments}` } + end +end diff --git a/railties/test/test_unit/reporter_test.rb b/railties/test/test_unit/reporter_test.rb index fa6bb71c64..e517d8dd0b 100644 --- a/railties/test/test_unit/reporter_test.rb +++ b/railties/test/test_unit/reporter_test.rb @@ -15,7 +15,7 @@ class TestUnitReporterTest < ActiveSupport::TestCase @reporter.record(failed_test) @reporter.report - assert_match %r{^bin/rails test .*test/test_unit/reporter_test.rb:6$}, @output.string + assert_match %r{^bin/rails test .*test/test_unit/reporter_test.rb:\d+$}, @output.string assert_rerun_snippet_count 1 end @@ -51,7 +51,7 @@ class TestUnitReporterTest < ActiveSupport::TestCase @reporter.record(failed_test) @reporter.report - assert_match %r{^bin/test .*test/test_unit/reporter_test.rb:6$}, @output.string + assert_match %r{^bin/test .*test/test_unit/reporter_test.rb:\d+$}, @output.string ensure Rails::TestUnitReporter.executable = original_executable end @@ -61,7 +61,7 @@ class TestUnitReporterTest < ActiveSupport::TestCase @reporter.record(failed_test) @reporter.report - assert_match %r{\A\n\nboo\n\nbin/rails test .*test/test_unit/reporter_test.rb:6\n\n\z}, @output.string + assert_match %r{\A\n\nboo\n\nbin/rails test .*test/test_unit/reporter_test.rb:\d+\n\n\z}, @output.string end test "outputs errors inline" do @@ -76,7 +76,7 @@ class TestUnitReporterTest < ActiveSupport::TestCase verbose.record(skipped_test) verbose.report - assert_match %r{\A\n\nskipchurches, misstemples\n\nbin/rails test .*test/test_unit/reporter_test.rb:6\n\n\z}, @output.string + assert_match %r{\A\n\nskipchurches, misstemples\n\nbin/rails test .*test/test_unit/reporter_test.rb:\d+\n\n\z}, @output.string end test "does not output rerun snippets after run" do diff --git a/tasks/release.rb b/tasks/release.rb index 5f0f4a06c1..7c8b884d44 100644 --- a/tasks/release.rb +++ b/tasks/release.rb @@ -109,7 +109,7 @@ namespace :all do task :push => FRAMEWORKS.map { |f| "#{f}:push" } + ['rails:push'] task :ensure_clean_state do - unless `git status -s | grep -v 'RAILS_VERSION\\|CHANGELOG'`.strip.empty? + unless `git status -s | grep -v 'RAILS_VERSION\\|CHANGELOG\\|Gemfile.lock'`.strip.empty? abort "[ABORTING] `git status` reports a dirty tree. Make sure all changes are committed" end |