diff options
121 files changed, 2408 insertions, 1408 deletions
@@ -7,6 +7,9 @@ gem "rake", ">= 0.8.7" gem "mocha", ">= 0.9.8" group :mri do + gem 'json' + gem 'yajl-ruby' + if RUBY_VERSION < '1.9' gem "system_timer" gem "ruby-debug", ">= 0.10.3" diff --git a/actionmailer/lib/action_mailer/base.rb b/actionmailer/lib/action_mailer/base.rb index d827ccdf2b..e566132f4e 100644 --- a/actionmailer/lib/action_mailer/base.rb +++ b/actionmailer/lib/action_mailer/base.rb @@ -291,8 +291,6 @@ module ActionMailer #:nodoc: :parts_order => [ "text/plain", "text/enriched", "text/html" ] }.freeze - ActiveSupport.run_load_hooks(:action_mailer, self) - class << self def mailer_name @@ -643,5 +641,6 @@ module ActionMailer #:nodoc: container.add_part(part) end + ActiveSupport.run_load_hooks(:action_mailer, self) end end diff --git a/actionmailer/test/old_base/mail_service_test.rb b/actionmailer/test/old_base/mail_service_test.rb index db2db59cc7..9da9132fe1 100644 --- a/actionmailer/test/old_base/mail_service_test.rb +++ b/actionmailer/test/old_base/mail_service_test.rb @@ -866,7 +866,6 @@ EOF regex = Regexp.escape('Subject: Foo =?UTF-8?Q?=C3=A1=C3=AB=C3=B4=?= =?UTF-8?Q?_=C3=AE=C3=BC=?=') assert_match(/#{regex}/, mail.encoded) string = "Foo áëô îü" - string.force_encoding('UTF-8') if string.respond_to?(:force_encoding) assert_match(string, mail.subject) end @@ -875,7 +874,6 @@ EOF regex = Regexp.escape('Subject: Foo =?UTF-8?Q?=C3=A1=C3=AB=C3=B4=?= =?UTF-8?Q?_=C3=AE=C3=BC=?=') assert_match(/#{regex}/, mail.encoded) string = "Foo áëô îü" - string.force_encoding('UTF-8') if string.respond_to?(:force_encoding) assert_match(string, mail.subject) end diff --git a/actionpack/CHANGELOG b/actionpack/CHANGELOG index 064e06bf92..04e44be291 100644 --- a/actionpack/CHANGELOG +++ b/actionpack/CHANGELOG @@ -1,5 +1,7 @@ *Rails 3.0.0 [beta 4/release candidate] (unreleased)* +* Renamed the field error CSS class from fieldWithErrors to field_with_errors for consistency. [Jeremy Kemper] + * Add support for shorthand routes like /projects/status(.:format) #4423 [Diego Carrion] * Changed translate helper so that it doesn’t mark every translation as safe HTML. Only keys with a "_html" suffix and keys named "html" are considered to be safe HTML. All other translations are left untouched. [Craig Davey] diff --git a/actionpack/lib/abstract_controller.rb b/actionpack/lib/abstract_controller.rb index de95f935c2..2da4dc052c 100644 --- a/actionpack/lib/abstract_controller.rb +++ b/actionpack/lib/abstract_controller.rb @@ -12,7 +12,6 @@ require 'active_support/i18n' module AbstractController extend ActiveSupport::Autoload - autoload :Assigns autoload :Base autoload :Callbacks autoload :Collector diff --git a/actionpack/lib/abstract_controller/assigns.rb b/actionpack/lib/abstract_controller/assigns.rb deleted file mode 100644 index 21459c6d51..0000000000 --- a/actionpack/lib/abstract_controller/assigns.rb +++ /dev/null @@ -1,21 +0,0 @@ -module AbstractController - module Assigns - # This method should return a hash with assigns. - # You can overwrite this configuration per controller. - # :api: public - def view_assigns - hash = {} - variables = instance_variable_names - variables -= protected_instance_variables if respond_to?(:protected_instance_variables) - variables.each { |name| hash[name] = instance_variable_get(name) } - hash - end - - # This method assigns the hash specified in _assigns_hash to the given object. - # :api: private - # TODO Ideally, this should be done on AV::Base.new initialization. - def _evaluate_assigns(object) - view_assigns.each { |k,v| object.instance_variable_set(k, v) } - end - end -end
\ No newline at end of file diff --git a/actionpack/lib/abstract_controller/base.rb b/actionpack/lib/abstract_controller/base.rb index c12b584144..8500cbd7f2 100644 --- a/actionpack/lib/abstract_controller/base.rb +++ b/actionpack/lib/abstract_controller/base.rb @@ -1,4 +1,4 @@ -require 'active_support/ordered_options' +require 'active_support/configurable' module AbstractController class Error < StandardError; end @@ -8,6 +8,8 @@ module AbstractController attr_internal :response_body attr_internal :action_name + include ActiveSupport::Configurable + class << self attr_reader :abstract alias_method :abstract?, :abstract @@ -29,14 +31,6 @@ module AbstractController @descendants ||= [] end - def config - @config ||= ActiveSupport::InheritableOptions.new(superclass < Base ? superclass.config : {}) - end - - def configure - yield config - end - # A list of all internal methods for a controller. This finds the first # abstract superclass of a controller, and gets a list of all public # instance methods on that abstract class. Public instance methods of @@ -99,10 +93,6 @@ module AbstractController abstract! - def config - @config ||= ActiveSupport::InheritableOptions.new(self.class.config) - end - # Calls the action going through the entire action dispatch stack. # # The actual method that is called is determined by calling @@ -133,6 +123,7 @@ module AbstractController end private + # Returns true if the name can be considered an action. This can # be overridden in subclasses to modify the semantics of what # can be considered an action. diff --git a/actionpack/lib/abstract_controller/helpers.rb b/actionpack/lib/abstract_controller/helpers.rb index 53cf6b3931..4374b439d0 100644 --- a/actionpack/lib/abstract_controller/helpers.rb +++ b/actionpack/lib/abstract_controller/helpers.rb @@ -8,7 +8,6 @@ module AbstractController included do class_attribute :_helpers - delegate :_helpers, :to => :'self.class' self._helpers = Module.new end diff --git a/actionpack/lib/abstract_controller/logger.rb b/actionpack/lib/abstract_controller/logger.rb index 9318f5e369..0b196119f4 100644 --- a/actionpack/lib/abstract_controller/logger.rb +++ b/actionpack/lib/abstract_controller/logger.rb @@ -6,7 +6,7 @@ module AbstractController extend ActiveSupport::Concern included do - cattr_accessor :logger + config_accessor :logger extend ActiveSupport::Benchmarkable end end diff --git a/actionpack/lib/action_controller/base.rb b/actionpack/lib/action_controller/base.rb index 2e94a20d9d..092fe98588 100644 --- a/actionpack/lib/action_controller/base.rb +++ b/actionpack/lib/action_controller/base.rb @@ -65,8 +65,11 @@ module ActionController @subclasses ||= [] end + # TODO Move this to the appropriate module + config_accessor :assets_dir, :asset_path, :javascripts_dir, :stylesheets_dir + ActiveSupport.run_load_hooks(:action_controller, self) end end -require "action_controller/deprecated/base" +require "action_controller/deprecated/base"
\ No newline at end of file diff --git a/actionpack/lib/action_controller/caching.rb b/actionpack/lib/action_controller/caching.rb index 0da0ca1893..4105f9e14f 100644 --- a/actionpack/lib/action_controller/caching.rb +++ b/actionpack/lib/action_controller/caching.rb @@ -63,12 +63,10 @@ module ActionController #:nodoc: included do extend ConfigMethods - delegate :perform_caching, :perform_caching=, :to => :config - singleton_class.delegate :perform_caching, :perform_caching=, :to => :config - self.perform_caching = true + config_accessor :perform_caching + self.perform_caching = true if perform_caching.nil? end - def caching_allowed? request.get? && response.status == 200 end diff --git a/actionpack/lib/action_controller/caching/pages.rb b/actionpack/lib/action_controller/caching/pages.rb index 20df3a1a69..cefd1f48c0 100644 --- a/actionpack/lib/action_controller/caching/pages.rb +++ b/actionpack/lib/action_controller/caching/pages.rb @@ -44,8 +44,8 @@ module ActionController #:nodoc: # For Rails, this directory has already been set to Rails.public_path (which is usually set to <tt>Rails.root + "/public"</tt>). Changing # this setting can be useful to avoid naming conflicts with files in <tt>public/</tt>, but doing so will likely require configuring your # web server to look in the new location for cached files. - singleton_class.delegate :page_cache_directory, :page_cache_directory=, :to => :config - self.page_cache_directory = '' + config_accessor :page_cache_directory + self.page_cache_directory ||= '' ## # :singleton-method: @@ -53,8 +53,8 @@ module ActionController #:nodoc: # order to make it easy for the cached files to be picked up properly by the web server. By default, this cache extension is <tt>.html</tt>. # If you want something else, like <tt>.php</tt> or <tt>.shtml</tt>, just set Base.page_cache_extension. In cases where a request already has an # extension, such as <tt>.xml</tt> or <tt>.rss</tt>, page caching will not add an extension. This allows it to work well with RESTful apps. - singleton_class.delegate :page_cache_extension, :page_cache_extension=, :to => :config - self.page_cache_extension = '.html' + config_accessor :page_cache_extension + self.page_cache_extension ||= '.html' end module ClassMethods diff --git a/actionpack/lib/action_controller/deprecated/base.rb b/actionpack/lib/action_controller/deprecated/base.rb index 57203ce95f..3975afcaf0 100644 --- a/actionpack/lib/action_controller/deprecated/base.rb +++ b/actionpack/lib/action_controller/deprecated/base.rb @@ -1,33 +1,16 @@ module ActionController class Base - class << self - def deprecated_config_accessor(option, message = nil) - deprecated_config_reader(option, message) - deprecated_config_writer(option, message) + # Deprecated methods. Wrap them in a module so they can be overwritten by plugins + # (like the verify method.) + module DeprecatedBehavior #:nodoc: + def relative_url_root + ActiveSupport::Deprecation.warn "ActionController::Base.relative_url_root is ineffective. " << + "Please stop using it.", caller end - def deprecated_config_reader(option, message = nil) - message ||= "Reading #{option} directly from ActionController::Base is deprecated. " \ - "Please read it from config.#{option}" - - self.class_eval <<-RUBY, __FILE__, __LINE__ + 1 - def #{option} - ActiveSupport::Deprecation.warn #{message.inspect}, caller - config.#{option} - end - RUBY - end - - def deprecated_config_writer(option, message = nil) - message ||= "Setting #{option} directly on ActionController::Base is deprecated. " \ - "Please set it on config.action_controller.#{option}" - - self.class_eval <<-RUBY, __FILE__, __LINE__ + 1 - def #{option}=(val) - ActiveSupport::Deprecation.warn #{message.inspect}, caller - config.#{option} = val - end - RUBY + def relative_url_root= + ActiveSupport::Deprecation.warn "ActionController::Base.relative_url_root= is ineffective. " << + "Please stop using it.", caller end def consider_all_requests_local @@ -125,9 +108,7 @@ module ActionController def use_accept_header=(val) use_accept_header end - end - module DeprecatedBehavior # This method has been moved to ActionDispatch::Request.filter_parameters def filter_parameter_logging(*args, &block) ActiveSupport::Deprecation.warn("Setting filter_parameter_logging in ActionController is deprecated and has no longer effect, please set 'config.filter_parameters' in config/application.rb instead", caller) @@ -146,17 +127,6 @@ module ActionController extend DeprecatedBehavior - deprecated_config_writer :session_options - deprecated_config_writer :session_store - - deprecated_config_accessor :assets_dir - deprecated_config_accessor :asset_path - deprecated_config_accessor :helpers_path - deprecated_config_accessor :javascripts_dir - deprecated_config_accessor :page_cache_directory - deprecated_config_accessor :relative_url_root, "relative_url_root is ineffective. Please stop using it" - deprecated_config_accessor :stylesheets_dir - delegate :consider_all_requests_local, :consider_all_requests_local=, :allow_concurrency, :allow_concurrency=, :to => :"self.class" end diff --git a/actionpack/lib/action_controller/metal/compatibility.rb b/actionpack/lib/action_controller/metal/compatibility.rb index 02722360f1..d49465fa0b 100644 --- a/actionpack/lib/action_controller/metal/compatibility.rb +++ b/actionpack/lib/action_controller/metal/compatibility.rb @@ -21,8 +21,8 @@ module ActionController delegate :default_charset=, :to => "ActionDispatch::Response" end - # cattr_reader :protected_instance_variables - cattr_accessor :protected_instance_variables + # TODO: Update protected instance variables list + config_accessor :protected_instance_variables self.protected_instance_variables = %w(@assigns @performed_redirect @performed_render @variables_added @request_origin @url @parent_controller @action_name diff --git a/actionpack/lib/action_controller/metal/helpers.rb b/actionpack/lib/action_controller/metal/helpers.rb index 82bedc3fad..1110d7c117 100644 --- a/actionpack/lib/action_controller/metal/helpers.rb +++ b/actionpack/lib/action_controller/metal/helpers.rb @@ -52,8 +52,8 @@ module ActionController include AbstractController::Helpers included do - class_attribute :helpers_path - self.helpers_path = [] + config_accessor :helpers_path + self.helpers_path ||= [] end module ClassMethods diff --git a/actionpack/lib/action_controller/metal/renderers.rb b/actionpack/lib/action_controller/metal/renderers.rb index aebd71e867..0be07cd1fc 100644 --- a/actionpack/lib/action_controller/metal/renderers.rb +++ b/actionpack/lib/action_controller/metal/renderers.rb @@ -71,7 +71,7 @@ module ActionController end add :json do |json, options| - json = ActiveSupport::JSON.encode(json) unless json.respond_to?(:to_str) + json = ActiveSupport::JSON.encode(json, options) unless json.respond_to?(:to_str) json = "#{options[:callback]}(#{json})" unless options[:callback].blank? self.content_type ||= Mime::JSON self.response_body = json @@ -79,12 +79,12 @@ module ActionController add :js do |js, options| self.content_type ||= Mime::JS - self.response_body = js.respond_to?(:to_js) ? js.to_js : js + self.response_body = js.respond_to?(:to_js) ? js.to_js(options) : js end add :xml do |xml, options| self.content_type ||= Mime::XML - self.response_body = xml.respond_to?(:to_xml) ? xml.to_xml : xml + self.response_body = xml.respond_to?(:to_xml) ? xml.to_xml(options) : xml end add :update do |proc, options| diff --git a/actionpack/lib/action_controller/metal/request_forgery_protection.rb b/actionpack/lib/action_controller/metal/request_forgery_protection.rb index 39a809657b..2ba0d6e5cd 100644 --- a/actionpack/lib/action_controller/metal/request_forgery_protection.rb +++ b/actionpack/lib/action_controller/metal/request_forgery_protection.rb @@ -4,6 +4,45 @@ module ActionController #:nodoc: class InvalidAuthenticityToken < ActionControllerError #:nodoc: end + # Protecting controller actions from CSRF attacks by ensuring that all forms are coming from the current + # web application, not a forged link from another site, is done by embedding a token based on a random + # string stored in the session (which an attacker wouldn't know) in all forms and Ajax requests generated + # by Rails and then verifying the authenticity of that token in the controller. Only HTML/JavaScript + # requests are checked, so this will not protect your XML API (presumably you'll have a different + # authentication scheme there anyway). Also, GET requests are not protected as these should be + # idempotent anyway. + # + # This is turned on with the <tt>protect_from_forgery</tt> method, which will check the token and raise an + # ActionController::InvalidAuthenticityToken if it doesn't match what was expected. You can customize the + # error message in production by editing public/422.html. A call to this method in ApplicationController is + # generated by default in post-Rails 2.0 applications. + # + # The token parameter is named <tt>authenticity_token</tt> by default. If you are generating an HTML form + # manually (without the use of Rails' <tt>form_for</tt>, <tt>form_tag</tt> or other helpers), you have to + # include a hidden field named like that and set its value to what is returned by + # <tt>form_authenticity_token</tt>. + # + # Request forgery protection is disabled by default in test environment. If you are upgrading from Rails + # 1.x, add this to config/environments/test.rb: + # + # # Disable request forgery protection in test environment + # config.action_controller.allow_forgery_protection = false + # + # == Learn more about CSRF (Cross-Site Request Forgery) attacks + # + # Here are some resources: + # * http://isc.sans.org/diary.html?storyid=1750 + # * http://en.wikipedia.org/wiki/Cross-site_request_forgery + # + # Keep in mind, this is NOT a silver-bullet, plug 'n' play, warm security blanket for your rails application. + # There are a few guidelines you should follow: + # + # * Keep your GET requests safe and idempotent. More reading material: + # * http://www.xml.com/pub/a/2002/04/24/deviant.html + # * http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.1.1 + # * Make sure the session cookies that Rails creates are non-persistent. Check in Firefox and look + # for "Expires: at end of session" + # module RequestForgeryProtection extend ActiveSupport::Concern @@ -12,54 +51,17 @@ module ActionController #:nodoc: included do # Sets the token parameter name for RequestForgery. Calling +protect_from_forgery+ # sets it to <tt>:authenticity_token</tt> by default. - config.request_forgery_protection_token ||= :authenticity_token + config_accessor :request_forgery_protection_token + self.request_forgery_protection_token ||= :authenticity_token # Controls whether request forgergy protection is turned on or not. Turned off by default only in test mode. - config.allow_forgery_protection ||= true + config_accessor :allow_forgery_protection + self.allow_forgery_protection = true if allow_forgery_protection.nil? helper_method :form_authenticity_token helper_method :protect_against_forgery? end - # Protecting controller actions from CSRF attacks by ensuring that all forms are coming from the current - # web application, not a forged link from another site, is done by embedding a token based on a random - # string stored in the session (which an attacker wouldn't know) in all forms and Ajax requests generated - # by Rails and then verifying the authenticity of that token in the controller. Only HTML/JavaScript - # requests are checked, so this will not protect your XML API (presumably you'll have a different - # authentication scheme there anyway). Also, GET requests are not protected as these should be - # idempotent anyway. - # - # This is turned on with the <tt>protect_from_forgery</tt> method, which will check the token and raise an - # ActionController::InvalidAuthenticityToken if it doesn't match what was expected. You can customize the - # error message in production by editing public/422.html. A call to this method in ApplicationController is - # generated by default in post-Rails 2.0 applications. - # - # The token parameter is named <tt>authenticity_token</tt> by default. If you are generating an HTML form - # manually (without the use of Rails' <tt>form_for</tt>, <tt>form_tag</tt> or other helpers), you have to - # include a hidden field named like that and set its value to what is returned by - # <tt>form_authenticity_token</tt>. - # - # Request forgery protection is disabled by default in test environment. If you are upgrading from Rails - # 1.x, add this to config/environments/test.rb: - # - # # Disable request forgery protection in test environment - # config.action_controller.allow_forgery_protection = false - # - # == Learn more about CSRF (Cross-Site Request Forgery) attacks - # - # Here are some resources: - # * http://isc.sans.org/diary.html?storyid=1750 - # * http://en.wikipedia.org/wiki/Cross-site_request_forgery - # - # Keep in mind, this is NOT a silver-bullet, plug 'n' play, warm security blanket for your rails application. - # There are a few guidelines you should follow: - # - # * Keep your GET requests safe and idempotent. More reading material: - # * http://www.xml.com/pub/a/2002/04/24/deviant.html - # * http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.1.1 - # * Make sure the session cookies that Rails creates are non-persistent. Check in Firefox and look - # for "Expires: at end of session" - # module ClassMethods # Turn on request forgery protection. Bear in mind that only non-GET, HTML/JavaScript requests are checked. # @@ -79,22 +81,6 @@ module ActionController #:nodoc: self.request_forgery_protection_token ||= :authenticity_token before_filter :verify_authenticity_token, options end - - def request_forgery_protection_token - config.request_forgery_protection_token - end - - def request_forgery_protection_token=(val) - config.request_forgery_protection_token = val - end - - def allow_forgery_protection - config.allow_forgery_protection - end - - def allow_forgery_protection=(val) - config.allow_forgery_protection = val - end end protected @@ -104,22 +90,6 @@ module ActionController #:nodoc: before_filter :verify_authenticity_token, options end - def request_forgery_protection_token - config.request_forgery_protection_token - end - - def request_forgery_protection_token=(val) - config.request_forgery_protection_token = val - end - - def allow_forgery_protection - config.allow_forgery_protection - end - - def allow_forgery_protection=(val) - config.allow_forgery_protection = val - end - # The actual before_filter that is used. Modify this to change how you handle unverified requests. def verify_authenticity_token verified_request? || raise(ActionController::InvalidAuthenticityToken) @@ -146,7 +116,7 @@ module ActionController #:nodoc: end def protect_against_forgery? - config.allow_forgery_protection + allow_forgery_protection end end end diff --git a/actionpack/lib/action_controller/railtie.rb b/actionpack/lib/action_controller/railtie.rb index b029434004..0e3cdffadc 100644 --- a/actionpack/lib/action_controller/railtie.rb +++ b/actionpack/lib/action_controller/railtie.rb @@ -13,64 +13,51 @@ module ActionController class Railtie < Rails::Railtie config.action_controller = ActiveSupport::OrderedOptions.new - ad = config.action_dispatch - config.action_controller.singleton_class.send(:define_method, :session) do - ActiveSupport::Deprecation.warn "config.action_controller.session has been " \ - "renamed to config.action_dispatch.session.", caller - ad.session - end + config.action_controller.singleton_class.tap do |d| + d.send(:define_method, :session) do + ActiveSupport::Deprecation.warn "config.action_controller.session has been deprecated. " << + "Please use Rails.application.config.session_store instead.", caller + end - config.action_controller.singleton_class.send(:define_method, :session=) do |val| - ActiveSupport::Deprecation.warn "config.action_controller.session has been " \ - "renamed to config.action_dispatch.session.", caller - ad.session = val - end + d.send(:define_method, :session=) do |val| + ActiveSupport::Deprecation.warn "config.action_controller.session= has been deprecated. " << + "Please use config.session_store(name, options) instead.", caller + end - config.action_controller.singleton_class.send(:define_method, :session_store) do - ActiveSupport::Deprecation.warn "config.action_controller.session_store has been " \ - "renamed to config.action_dispatch.session_store.", caller - ad.session_store - end + d.send(:define_method, :session_store) do + ActiveSupport::Deprecation.warn "config.action_controller.session_store has been deprecated. " << + "Please use Rails.application.config.session_store instead.", caller + end - config.action_controller.singleton_class.send(:define_method, :session_store=) do |val| - ActiveSupport::Deprecation.warn "config.action_controller.session_store has been " \ - "renamed to config.action_dispatch.session_store.", caller - ad.session_store = val + d.send(:define_method, :session_store=) do |val| + ActiveSupport::Deprecation.warn "config.action_controller.session_store= has been deprecated. " << + "Please use config.session_store(name, options) instead.", caller + end end log_subscriber :action_controller, ActionController::Railties::LogSubscriber.new - initializer "action_controller.logger" do - ActiveSupport.on_load(:action_controller) { self.logger ||= Rails.logger } - end - - initializer "action_controller.page_cache_directory" do - ActiveSupport.on_load(:action_controller) do - self.page_cache_directory = Rails.public_path - end - end - initializer "action_controller.set_configs" do |app| paths = app.config.paths ac = app.config.action_controller - ac.assets_dir = paths.public.to_a.first - ac.javascripts_dir = paths.public.javascripts.to_a.first - ac.stylesheets_dir = paths.public.stylesheets.to_a.first + ac.assets_dir ||= paths.public.to_a.first + ac.javascripts_dir ||= paths.public.javascripts.to_a.first + ac.stylesheets_dir ||= paths.public.stylesheets.to_a.first + ac.page_cache_directory ||= paths.public.to_a.first + ac.helpers_path ||= paths.app.helpers.to_a ActiveSupport.on_load(:action_controller) do self.config.merge!(ac) end end - initializer "action_controller.initialize_framework_caches" do - ActiveSupport.on_load(:action_controller) { self.cache_store ||= RAILS_CACHE } + initializer "action_controller.logger" do + ActiveSupport.on_load(:action_controller) { self.logger ||= Rails.logger } end - initializer "action_controller.set_helpers_path" do |app| - ActiveSupport.on_load(:action_controller) do - self.helpers_path = app.config.paths.app.helpers.to_a - end + initializer "action_controller.initialize_framework_caches" do + ActiveSupport.on_load(:action_controller) { self.cache_store ||= RAILS_CACHE } end initializer "action_controller.url_helpers" do |app| diff --git a/actionpack/lib/action_controller/test_case.rb b/actionpack/lib/action_controller/test_case.rb index b3758218a2..2b9b34961e 100644 --- a/actionpack/lib/action_controller/test_case.rb +++ b/actionpack/lib/action_controller/test_case.rb @@ -36,6 +36,7 @@ module ActionController end def teardown_subscriptions + ActiveSupport::Notifications.unsubscribe("action_view.render_template") ActiveSupport::Notifications.unsubscribe("action_view.render_template!") end @@ -282,165 +283,143 @@ module ActionController # # assert_redirected_to page_url(:title => 'foo') class TestCase < ActiveSupport::TestCase - include ActionDispatch::TestProcess - include ActionController::TemplateAssertions + module Behavior + extend ActiveSupport::Concern + include ActionDispatch::TestProcess - attr_reader :response, :request + attr_reader :response, :request - # Executes a request simulating GET HTTP method and set/volley the response - def get(action, parameters = nil, session = nil, flash = nil) - process(action, parameters, session, flash, "GET") - end + module ClassMethods - # Executes a request simulating POST HTTP method and set/volley the response - def post(action, parameters = nil, session = nil, flash = nil) - process(action, parameters, session, flash, "POST") - end + # Sets the controller class name. Useful if the name can't be inferred from test class. + # Expects +controller_class+ as a constant. Example: <tt>tests WidgetController</tt>. + def tests(controller_class) + self.controller_class = controller_class + end + + def controller_class=(new_class) + prepare_controller_class(new_class) if new_class + write_inheritable_attribute(:controller_class, new_class) + end - # Executes a request simulating PUT HTTP method and set/volley the response - def put(action, parameters = nil, session = nil, flash = nil) - process(action, parameters, session, flash, "PUT") - end + def controller_class + if current_controller_class = read_inheritable_attribute(:controller_class) + current_controller_class + else + self.controller_class = determine_default_controller_class(name) + end + end - # Executes a request simulating DELETE HTTP method and set/volley the response - def delete(action, parameters = nil, session = nil, flash = nil) - process(action, parameters, session, flash, "DELETE") - end + def determine_default_controller_class(name) + name.sub(/Test$/, '').constantize + rescue NameError + nil + end - # Executes a request simulating HEAD HTTP method and set/volley the response - def head(action, parameters = nil, session = nil, flash = nil) - process(action, parameters, session, flash, "HEAD") - end + def prepare_controller_class(new_class) + new_class.send :include, ActionController::TestCase::RaiseActionExceptions + end - def xml_http_request(request_method, action, parameters = nil, session = nil, flash = nil) - @request.env['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest' - @request.env['HTTP_ACCEPT'] ||= [Mime::JS, Mime::HTML, Mime::XML, 'text/xml', Mime::ALL].join(', ') - returning __send__(request_method, action, parameters, session, flash) do - @request.env.delete 'HTTP_X_REQUESTED_WITH' - @request.env.delete 'HTTP_ACCEPT' end - end - alias xhr :xml_http_request - - def process(action, parameters = nil, session = nil, flash = nil, http_method = 'GET') - # Sanity check for required instance variables so we can give an - # understandable error message. - %w(@routes @controller @request @response).each do |iv_name| - if !(instance_variable_names.include?(iv_name) || instance_variable_names.include?(iv_name.to_sym)) || instance_variable_get(iv_name).nil? - raise "#{iv_name} is nil: make sure you set it in your test's setup method." - end + + # Executes a request simulating GET HTTP method and set/volley the response + def get(action, parameters = nil, session = nil, flash = nil) + process(action, parameters, session, flash, "GET") end - @request.recycle! - @response.recycle! - @controller.response_body = nil - @controller.formats = nil - @controller.params = nil - - @html_document = nil - @request.env['REQUEST_METHOD'] = http_method - - parameters ||= {} - @request.assign_parameters(@routes, @controller.class.name.underscore.sub(/_controller$/, ''), action.to_s, parameters) - - @request.session = ActionController::TestSession.new(session) unless session.nil? - @request.session["flash"] = @request.flash.update(flash || {}) - @request.session["flash"].sweep - - @controller.request = @request - @controller.params.merge!(parameters) - build_request_uri(action, parameters) - Base.class_eval { include Testing } - @controller.process_with_new_base_test(@request, @response) - @request.session.delete('flash') if @request.session['flash'].blank? - @response - end + # Executes a request simulating POST HTTP method and set/volley the response + def post(action, parameters = nil, session = nil, flash = nil) + process(action, parameters, session, flash, "POST") + end - include ActionDispatch::Assertions + # Executes a request simulating PUT HTTP method and set/volley the response + def put(action, parameters = nil, session = nil, flash = nil) + process(action, parameters, session, flash, "PUT") + end - # When the request.remote_addr remains the default for testing, which is 0.0.0.0, the exception is simply raised inline - # (bystepping the regular exception handling from rescue_action). If the request.remote_addr is anything else, the regular - # rescue_action process takes place. This means you can test your rescue_action code by setting remote_addr to something else - # than 0.0.0.0. - # - # The exception is stored in the exception accessor for further inspection. - module RaiseActionExceptions - def self.included(base) - base.class_eval do - attr_accessor :exception - protected :exception, :exception= - end + # Executes a request simulating DELETE HTTP method and set/volley the response + def delete(action, parameters = nil, session = nil, flash = nil) + process(action, parameters, session, flash, "DELETE") end - protected - def rescue_action_without_handler(e) - self.exception = e + # Executes a request simulating HEAD HTTP method and set/volley the response + def head(action, parameters = nil, session = nil, flash = nil) + process(action, parameters, session, flash, "HEAD") + end - if request.remote_addr == "0.0.0.0" - raise(e) - else - super(e) + def xml_http_request(request_method, action, parameters = nil, session = nil, flash = nil) + @request.env['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest' + @request.env['HTTP_ACCEPT'] ||= [Mime::JS, Mime::HTML, Mime::XML, 'text/xml', Mime::ALL].join(', ') + returning __send__(request_method, action, parameters, session, flash) do + @request.env.delete 'HTTP_X_REQUESTED_WITH' + @request.env.delete 'HTTP_ACCEPT' + end + end + alias xhr :xml_http_request + + def process(action, parameters = nil, session = nil, flash = nil, http_method = 'GET') + # Sanity check for required instance variables so we can give an + # understandable error message. + %w(@routes @controller @request @response).each do |iv_name| + if !(instance_variable_names.include?(iv_name) || instance_variable_names.include?(iv_name.to_sym)) || instance_variable_get(iv_name).nil? + raise "#{iv_name} is nil: make sure you set it in your test's setup method." end end - end - setup :setup_controller_request_and_response + @request.recycle! + @response.recycle! + @controller.response_body = nil + @controller.formats = nil + @controller.params = nil - @@controller_class = nil + @html_document = nil + @request.env['REQUEST_METHOD'] = http_method - class << self - # Sets the controller class name. Useful if the name can't be inferred from test class. - # Expects +controller_class+ as a constant. Example: <tt>tests WidgetController</tt>. - def tests(controller_class) - self.controller_class = controller_class - end + parameters ||= {} + @request.assign_parameters(@routes, @controller.class.name.underscore.sub(/_controller$/, ''), action.to_s, parameters) - def controller_class=(new_class) - prepare_controller_class(new_class) if new_class - write_inheritable_attribute(:controller_class, new_class) - end + @request.session = ActionController::TestSession.new(session) unless session.nil? + @request.session["flash"] = @request.flash.update(flash || {}) + @request.session["flash"].sweep - def controller_class - if current_controller_class = read_inheritable_attribute(:controller_class) - current_controller_class - else - self.controller_class = determine_default_controller_class(name) - end + @controller.request = @request + @controller.params.merge!(parameters) + build_request_uri(action, parameters) + Base.class_eval { include Testing } + @controller.process_with_new_base_test(@request, @response) + @request.session.delete('flash') if @request.session['flash'].blank? + @response end - def determine_default_controller_class(name) - name.sub(/Test$/, '').constantize - rescue NameError - nil - end + def setup_controller_request_and_response + @request = TestRequest.new + @response = TestResponse.new - def prepare_controller_class(new_class) - new_class.send :include, RaiseActionExceptions - end - end + if klass = self.class.controller_class + @controller ||= klass.new rescue nil + end - def setup_controller_request_and_response - @request = TestRequest.new - @response = TestResponse.new + @request.env.delete('PATH_INFO') - if klass = self.class.controller_class - @controller ||= klass.new rescue nil + if @controller + @controller.request = @request + @controller.params = {} + end end - @request.env.delete('PATH_INFO') - - if @controller - @controller.request = @request - @controller.params = {} + # Cause the action to be rescued according to the regular rules for rescue_action when the visitor is not local + def rescue_action_in_public! + @request.remote_addr = '208.77.188.166' # example.com end - end - # Cause the action to be rescued according to the regular rules for rescue_action when the visitor is not local - def rescue_action_in_public! - @request.remote_addr = '208.77.188.166' # example.com - end + included do + include ActionController::TemplateAssertions + include ActionDispatch::Assertions + setup :setup_controller_request_and_response + end private + def build_request_uri(action, parameters) unless @request.env["PATH_INFO"] options = @controller.__send__(:url_options).merge(parameters) @@ -458,4 +437,33 @@ module ActionController end end end + + # When the request.remote_addr remains the default for testing, which is 0.0.0.0, the exception is simply raised inline + # (bystepping the regular exception handling from rescue_action). If the request.remote_addr is anything else, the regular + # rescue_action process takes place. This means you can test your rescue_action code by setting remote_addr to something else + # than 0.0.0.0. + # + # The exception is stored in the exception accessor for further inspection. + module RaiseActionExceptions + def self.included(base) + base.class_eval do + attr_accessor :exception + protected :exception, :exception= + end + end + + protected + def rescue_action_without_handler(e) + self.exception = e + + if request.remote_addr == "0.0.0.0" + raise(e) + else + super(e) + end + end + end + + include Behavior + end end diff --git a/actionpack/lib/action_dispatch/http/parameters.rb b/actionpack/lib/action_dispatch/http/parameters.rb index 40b40ea94e..bc43414e75 100644 --- a/actionpack/lib/action_dispatch/http/parameters.rb +++ b/actionpack/lib/action_dispatch/http/parameters.rb @@ -1,4 +1,5 @@ require 'active_support/core_ext/hash/keys' +require 'active_support/core_ext/hash/indifferent_access' module ActionDispatch module Http @@ -46,4 +47,4 @@ module ActionDispatch end end end -end
\ No newline at end of file +end diff --git a/actionpack/lib/action_dispatch/middleware/params_parser.rb b/actionpack/lib/action_dispatch/middleware/params_parser.rb index 09fb1f998a..a25089176c 100644 --- a/actionpack/lib/action_dispatch/middleware/params_parser.rb +++ b/actionpack/lib/action_dispatch/middleware/params_parser.rb @@ -1,5 +1,6 @@ -require 'active_support/core_ext/hash/conversions.rb' +require 'active_support/core_ext/hash/conversions' require 'action_dispatch/http/request' +require 'active_support/core_ext/hash/indifferent_access' module ActionDispatch class ParamsParser diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb index ef2826a4e8..4b02c2deb3 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -64,10 +64,11 @@ module ActionDispatch end path = normalize_path(path) + path_without_format = path.sub(/\(\.:format\)$/, '') - if using_match_shorthand?(path, options) - options[:to] ||= path[1..-1].sub(%r{/([^/]*)$}, '#\1').sub(%r{\(.*\)}, '') - options[:as] ||= path[1..-1].gsub("/", "_").sub(%r{\(.*\)}, '') + if using_match_shorthand?(path_without_format, options) + options[:to] ||= path_without_format[1..-1].sub(%r{/([^/]*)$}, '#\1') + options[:as] ||= path_without_format[1..-1].gsub("/", "_") end [ path, options ] @@ -80,7 +81,7 @@ module ActionDispatch # match "account/overview" def using_match_shorthand?(path, options) - path && options.except(:via, :anchor, :to, :as).empty? && path =~ %r{^/[\w+/?]+(\(.*\))*$} + path && options.except(:via, :anchor, :to, :as).empty? && path =~ %r{^/[\w\/]+$} end def normalize_path(path) diff --git a/actionpack/lib/action_dispatch/routing/url_for.rb b/actionpack/lib/action_dispatch/routing/url_for.rb index fb236dce53..db17615d92 100644 --- a/actionpack/lib/action_dispatch/routing/url_for.rb +++ b/actionpack/lib/action_dispatch/routing/url_for.rb @@ -128,7 +128,7 @@ module ActionDispatch when String options when nil, Hash - _router.url_for(url_options.merge(options || {})) + _router.url_for(url_options.merge((options || {}).symbolize_keys)) else polymorphic_url(options) end diff --git a/actionpack/lib/action_dispatch/testing/assertions/routing.rb b/actionpack/lib/action_dispatch/testing/assertions/routing.rb index b7e9b0c95a..1499c03bdf 100644 --- a/actionpack/lib/action_dispatch/testing/assertions/routing.rb +++ b/actionpack/lib/action_dispatch/testing/assertions/routing.rb @@ -1,4 +1,5 @@ require 'active_support/core_ext/hash/diff' +require 'active_support/core_ext/hash/indifferent_access' module ActionDispatch module Assertions diff --git a/actionpack/lib/action_dispatch/testing/test_process.rb b/actionpack/lib/action_dispatch/testing/test_process.rb index 79f309cae7..c56ebc6438 100644 --- a/actionpack/lib/action_dispatch/testing/test_process.rb +++ b/actionpack/lib/action_dispatch/testing/test_process.rb @@ -1,4 +1,5 @@ require 'action_dispatch/middleware/flash' +require 'active_support/core_ext/hash/indifferent_access' module ActionDispatch module TestProcess diff --git a/actionpack/lib/action_view/base.rb b/actionpack/lib/action_view/base.rb index be9791505e..4ac2ee52d6 100644 --- a/actionpack/lib/action_view/base.rb +++ b/actionpack/lib/action_view/base.rb @@ -176,8 +176,6 @@ module ActionView #:nodoc: delegate :logger, :to => 'ActionController::Base', :allow_nil => true end - ActiveSupport.run_load_hooks(:action_view, self) - attr_accessor :base_path, :assigns, :template_extension, :lookup_context attr_internal :captures, :request, :controller, :template, :config @@ -229,5 +227,7 @@ module ActionView #:nodoc: response.body_parts << part nil end + + ActiveSupport.run_load_hooks(:action_view, self) end end diff --git a/actionpack/lib/action_view/helpers/active_model_helper.rb b/actionpack/lib/action_view/helpers/active_model_helper.rb index a7650c0050..d9f09b5dc5 100644 --- a/actionpack/lib/action_view/helpers/active_model_helper.rb +++ b/actionpack/lib/action_view/helpers/active_model_helper.rb @@ -6,7 +6,7 @@ require 'active_support/core_ext/object/blank' module ActionView ActiveSupport.on_load(:action_view) do class ActionView::Base - @@field_error_proc = Proc.new{ |html_tag, instance| "<div class=\"fieldWithErrors\">#{html_tag}</div>".html_safe } + @@field_error_proc = Proc.new{ |html_tag, instance| "<div class=\"field_with_errors\">#{html_tag}</div>".html_safe } cattr_accessor :field_error_proc end end diff --git a/actionpack/lib/action_view/helpers/prototype_helper.rb b/actionpack/lib/action_view/helpers/prototype_helper.rb index ebe0b4e876..a798c3eaef 100644 --- a/actionpack/lib/action_view/helpers/prototype_helper.rb +++ b/actionpack/lib/action_view/helpers/prototype_helper.rb @@ -767,7 +767,7 @@ module ActionView end def grep(variable, pattern, &block) - enumerate :grep, :variable => variable, :return => true, :method_args => [pattern], :yield_args => %w(value index), &block + enumerate :grep, :variable => variable, :return => true, :method_args => [::ActiveSupport::JSON::Variable.new(pattern.inspect)], :yield_args => %w(value index), &block end def in_groups_of(variable, number, fill_with = nil) diff --git a/actionpack/lib/action_view/test_case.rb b/actionpack/lib/action_view/test_case.rb index beda7743bf..f6417d5c2c 100644 --- a/actionpack/lib/action_view/test_case.rb +++ b/actionpack/lib/action_view/test_case.rb @@ -31,8 +31,8 @@ module ActionView include ActionController::PolymorphicRoutes include ActionController::RecordIdentifier + include AbstractController::Helpers include ActionView::Helpers - include ActionController::Helpers class_inheritable_accessor :helper_class attr_accessor :controller, :output_buffer, :rendered diff --git a/actionpack/test/abstract_unit.rb b/actionpack/test/abstract_unit.rb index acf887ee4a..f3ff258016 100644 --- a/actionpack/test/abstract_unit.rb +++ b/actionpack/test/abstract_unit.rb @@ -143,6 +143,12 @@ class BasicController end end +class ActionDispatch::IntegrationTest < ActiveSupport::TestCase + setup do + @routes = SharedTestRoutes + end +end + class ActionController::IntegrationTest < ActiveSupport::TestCase def self.build_app(routes = nil) RoutedRackApp.new(routes || ActionDispatch::Routing::RouteSet.new) do |middleware| @@ -275,4 +281,4 @@ module ActionController class Base include SharedTestRoutes.url_helpers end -end
\ No newline at end of file +end diff --git a/actionpack/test/controller/assert_select_test.rb b/actionpack/test/controller/assert_select_test.rb index cb3e848dfa..4ef6fa4000 100644 --- a/actionpack/test/controller/assert_select_test.rb +++ b/actionpack/test/controller/assert_select_test.rb @@ -1,3 +1,4 @@ +# encoding: utf-8 #-- # Copyright (c) 2006 Assaf Arkin (http://labnotes.org) # Under MIT and/or CC By license. @@ -347,7 +348,6 @@ class AssertSelectTest < ActionController::TestCase assert_select str, :text => "\343\203\201\343\202\261\343\203\203\343\203\210" assert_select str, "\343\203\201\343\202\261\343\203\203\343\203\210" if str.respond_to?(:force_encoding) - str.force_encoding(Encoding::UTF_8) assert_select str, /\343\203\201..\343\203\210/u assert_raise(Assertion) { assert_select str, /\343\203\201.\343\203\210/u } else diff --git a/actionpack/test/controller/layout_test.rb b/actionpack/test/controller/layout_test.rb index e1c1128753..48be7571ea 100644 --- a/actionpack/test/controller/layout_test.rb +++ b/actionpack/test/controller/layout_test.rb @@ -1,4 +1,5 @@ require 'abstract_unit' +require 'rbconfig' # The view_paths array must be set on Base and not LayoutTest so that LayoutTest's inherited # method has access to the view_paths array when looking for a layout to automatically assign. @@ -209,7 +210,7 @@ class LayoutStatusIsRenderedTest < ActionController::TestCase end end -unless RUBY_PLATFORM =~ /mswin|mingw/ +unless Config::CONFIG['host_os'] =~ /mswin|mingw/ class LayoutSymlinkedTest < LayoutTest layout "symlinked/symlinked_layout" end diff --git a/actionpack/test/controller/render_json_test.rb b/actionpack/test/controller/render_json_test.rb index 2580ada88b..5958b18d80 100644 --- a/actionpack/test/controller/render_json_test.rb +++ b/actionpack/test/controller/render_json_test.rb @@ -3,6 +3,14 @@ require 'controller/fake_models' require 'pathname' class RenderJsonTest < ActionController::TestCase + class JsonRenderable + def as_json(options={}) + hash = { :a => :b, :c => :d, :e => :f } + hash.except!(*options[:except]) if options[:except] + hash + end + end + class TestController < ActionController::Base protect_from_forgery @@ -37,6 +45,10 @@ class RenderJsonTest < ActionController::TestCase def render_json_with_render_to_string render :json => {:hello => render_to_string(:partial => 'partial')} end + + def render_json_with_extra_options + render :json => JsonRenderable.new, :except => [:c, :e] + end end tests TestController @@ -91,4 +103,10 @@ class RenderJsonTest < ActionController::TestCase assert_equal '{"hello":"partial html"}', @response.body assert_equal 'application/json', @response.content_type end + + def test_render_json_forwards_extra_options + get :render_json_with_extra_options + assert_equal '{"a":"b"}', @response.body + assert_equal 'application/json', @response.content_type + end end diff --git a/actionpack/test/controller/render_xml_test.rb b/actionpack/test/controller/render_xml_test.rb index 4da6c954cf..4bf867fa41 100644 --- a/actionpack/test/controller/render_xml_test.rb +++ b/actionpack/test/controller/render_xml_test.rb @@ -3,6 +3,13 @@ require 'controller/fake_models' require 'pathname' class RenderXmlTest < ActionController::TestCase + class XmlRenderable + def to_xml(options) + options[:root] ||= "i-am-xml" + "<#{options[:root]}/>" + end + end + class TestController < ActionController::Base protect_from_forgery @@ -20,13 +27,7 @@ class RenderXmlTest < ActionController::TestCase end def render_with_to_xml - to_xmlable = Class.new do - def to_xml - "<i-am-xml/>" - end - end.new - - render :xml => to_xmlable + render :xml => XmlRenderable.new end def formatted_xml_erb @@ -35,6 +36,10 @@ class RenderXmlTest < ActionController::TestCase def render_xml_with_custom_content_type render :xml => "<blah/>", :content_type => "application/atomsvc+xml" end + + def render_xml_with_custom_options + render :xml => XmlRenderable.new, :root => "i-am-THE-xml" + end end tests TestController @@ -58,6 +63,11 @@ class RenderXmlTest < ActionController::TestCase assert_equal "<i-am-xml/>", @response.body end + def test_rendering_xml_should_call_to_xml_with_extra_options + get :render_xml_with_custom_options + assert_equal "<i-am-THE-xml/>", @response.body + end + def test_rendering_with_object_location_should_set_header_with_url_for with_routing do |set| set.draw do |map| diff --git a/actionpack/test/controller/send_file_test.rb b/actionpack/test/controller/send_file_test.rb index 30c9a65b7c..36b8055810 100644 --- a/actionpack/test/controller/send_file_test.rb +++ b/actionpack/test/controller/send_file_test.rb @@ -25,7 +25,7 @@ class SendFileController < ActionController::Base end def multibyte_text_data - send_data("Кирилица\n祝您好運", options) + send_data("Кирилица\n祝您好運.", options) end end @@ -128,7 +128,7 @@ class SendFileTest < ActionController::TestCase assert_equal 'image/png', @controller.content_type end - + def test_send_file_headers_with_bad_symbol options = { diff --git a/actionpack/test/controller/url_for_test.rb b/actionpack/test/controller/url_for_test.rb index fc7773dffe..907acf9573 100644 --- a/actionpack/test/controller/url_for_test.rb +++ b/actionpack/test/controller/url_for_test.rb @@ -257,10 +257,24 @@ module AbstractController assert_equal second_class.default_url_options[:host], second_host end + def test_with_stringified_keys + assert_equal("/c", W.new.url_for('controller' => 'c', 'only_path' => true)) + assert_equal("/c/a", W.new.url_for('controller' => 'c', 'action' => 'a', 'only_path' => true)) + end + + def test_with_hash_with_indifferent_access + W.default_url_options[:controller] = 'd' + W.default_url_options[:only_path] = false + assert_equal("/c", W.new.url_for(HashWithIndifferentAccess.new('controller' => 'c', 'only_path' => true))) + + W.default_url_options[:action] = 'b' + assert_equal("/c/a", W.new.url_for(HashWithIndifferentAccess.new('controller' => 'c', 'action' => 'a', 'only_path' => true))) + end + private def extract_params(url) url.split('?', 2).last.split('&').sort end end end -end
\ No newline at end of file +end diff --git a/actionpack/test/dispatch/routing_test.rb b/actionpack/test/dispatch/routing_test.rb index 5bca476b27..b508996467 100644 --- a/actionpack/test/dispatch/routing_test.rb +++ b/actionpack/test/dispatch/routing_test.rb @@ -237,8 +237,8 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest AltRoutes = ActionDispatch::Routing::RouteSet.new(AltRequest) AltRoutes.draw do - get "/" => XHeader.new, :constraints => {:x_header => /HEADER/} - get "/" => AltApp.new + get "/" => TestRoutingMapper::TestAltApp::XHeader.new, :constraints => {:x_header => /HEADER/} + get "/" => TestRoutingMapper::TestAltApp::AltApp.new end def app @@ -1000,6 +1000,12 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end end + def test_assert_recognizes_account_overview + with_test_routes do + assert_recognizes({:controller => "account", :action => "overview"}, "/account/overview") + end + end + private def with_test_routes yield diff --git a/actionpack/test/template/active_model_helper_test.rb b/actionpack/test/template/active_model_helper_test.rb index 8deeb78eab..b1705072c2 100644 --- a/actionpack/test/template/active_model_helper_test.rb +++ b/actionpack/test/template/active_model_helper_test.rb @@ -27,14 +27,14 @@ class ActiveModelHelperTest < ActionView::TestCase def test_text_area_with_errors assert_dom_equal( - %(<div class="fieldWithErrors"><textarea cols="40" id="post_body" name="post[body]" rows="20">Back to the hill and over it again!</textarea></div>), + %(<div class="field_with_errors"><textarea cols="40" id="post_body" name="post[body]" rows="20">Back to the hill and over it again!</textarea></div>), text_area("post", "body") ) end def test_text_field_with_errors assert_dom_equal( - %(<div class="fieldWithErrors"><input id="post_author_name" name="post[author_name]" size="30" type="text" value="" /></div>), + %(<div class="field_with_errors"><input id="post_author_name" name="post[author_name]" size="30" type="text" value="" /></div>), text_field("post", "author_name") ) end @@ -42,11 +42,11 @@ class ActiveModelHelperTest < ActionView::TestCase def test_field_error_proc old_proc = ActionView::Base.field_error_proc ActionView::Base.field_error_proc = Proc.new do |html_tag, instance| - %(<div class=\"fieldWithErrors\">#{html_tag} <span class="error">#{[instance.error_message].join(', ')}</span></div>).html_safe + %(<div class=\"field_with_errors\">#{html_tag} <span class="error">#{[instance.error_message].join(', ')}</span></div>).html_safe end assert_dom_equal( - %(<div class="fieldWithErrors"><input id="post_author_name" name="post[author_name]" size="30" type="text" value="" /> <span class="error">can't be empty</span></div>), + %(<div class="field_with_errors"><input id="post_author_name" name="post[author_name]" size="30" type="text" value="" /> <span class="error">can't be empty</span></div>), text_field("post", "author_name") ) ensure diff --git a/actionpack/test/template/form_tag_helper_test.rb b/actionpack/test/template/form_tag_helper_test.rb index ef612b879b..8756bd310f 100644 --- a/actionpack/test/template/form_tag_helper_test.rb +++ b/actionpack/test/template/form_tag_helper_test.rb @@ -3,9 +3,6 @@ require 'abstract_unit' class FormTagHelperTest < ActionView::TestCase tests ActionView::Helpers::FormTagHelper - # include ActiveSupport::Configurable - # DEFAULT_CONFIG = ActionView::DEFAULT_CONFIG - def setup super @controller = BasicController.new diff --git a/actionpack/test/template/text_helper_test.rb b/actionpack/test/template/text_helper_test.rb index 9962b7af3f..8b6c107a21 100644 --- a/actionpack/test/template/text_helper_test.rb +++ b/actionpack/test/template/text_helper_test.rb @@ -1,3 +1,4 @@ +# encoding: us-ascii require 'abstract_unit' require 'testing_sandbox' begin diff --git a/actionpack/test/template/url_helper_test.rb b/actionpack/test/template/url_helper_test.rb index 4474949749..5120870f50 100644 --- a/actionpack/test/template/url_helper_test.rb +++ b/actionpack/test/template/url_helper_test.rb @@ -421,6 +421,10 @@ class UrlHelperControllerTest < ActionController::TestCase render :inline => "<%= url_for :controller => 'url_helper_controller_test/url_helper', :action => 'show_url_for' %>" end + def show_overriden_url_for + render :inline => "<%= url_for params.merge(:controller => 'url_helper_controller_test/url_helper', :action => 'show_url_for') %>" + end + def show_named_route render :inline => "<%= show_named_route_#{params[:kind]} %>" end @@ -439,6 +443,11 @@ class UrlHelperControllerTest < ActionController::TestCase assert_equal '/url_helper_controller_test/url_helper/show_url_for', @response.body end + def test_overriden_url_for_shows_only_path + get :show_overriden_url_for + assert_equal '/url_helper_controller_test/url_helper/show_url_for', @response.body + end + def test_named_route_url_shows_host_and_path get :show_named_route, :kind => 'url' assert_equal 'http://test.host/url_helper_controller_test/url_helper/show_named_route', diff --git a/activemodel/lib/active_model/locale/en.yml b/activemodel/lib/active_model/locale/en.yml index ea58021767..d05c04967c 100644 --- a/activemodel/lib/active_model/locale/en.yml +++ b/activemodel/lib/active_model/locale/en.yml @@ -17,6 +17,7 @@ en: too_short: "is too short (minimum is {{count}} characters)" wrong_length: "is the wrong length (should be {{count}} characters)" not_a_number: "is not a number" + not_an_integer: "must be an integer" greater_than: "must be greater than {{count}}" greater_than_or_equal_to: "must be greater than or equal to {{count}}" equal_to: "must be equal to {{count}}" diff --git a/activemodel/lib/active_model/serializers/xml.rb b/activemodel/lib/active_model/serializers/xml.rb index c226359ea7..ee3e0eab06 100644 --- a/activemodel/lib/active_model/serializers/xml.rb +++ b/activemodel/lib/active_model/serializers/xml.rb @@ -1,6 +1,7 @@ require 'active_support/core_ext/array/wrap' require 'active_support/core_ext/class/attribute_accessors' require 'active_support/core_ext/hash/conversions' +require 'active_support/core_ext/hash/slice' module ActiveModel module Serializers @@ -12,8 +13,10 @@ module ActiveModel class Attribute #:nodoc: attr_reader :name, :value, :type - def initialize(name, serializable) + def initialize(name, serializable, raw_value=nil) @name, @serializable = name, serializable + @raw_value = raw_value || @serializable.send(name) + @type = compute_type @value = compute_value end @@ -51,20 +54,17 @@ module ActiveModel protected def compute_type - value = @serializable.send(name) - type = Hash::XML_TYPE_NAMES[value.class.name] - type ||= :string if value.respond_to?(:to_str) + type = Hash::XML_TYPE_NAMES[@raw_value.class.name] + type ||= :string if @raw_value.respond_to?(:to_str) type ||= :yaml type end def compute_value - value = @serializable.send(name) - if formatter = Hash::XML_FORMATTING[type.to_s] - value ? formatter.call(value) : nil + @raw_value ? formatter.call(@raw_value) : nil else - value + @raw_value end end end @@ -72,7 +72,7 @@ module ActiveModel class MethodAttribute < Attribute #:nodoc: protected def compute_type - Hash::XML_TYPE_NAMES[@serializable.send(name).class.name] || :string + Hash::XML_TYPE_NAMES[@raw_value.class.name] || :string end end @@ -92,25 +92,24 @@ module ActiveModel # then because <tt>:except</tt> is set to a default value, the second # level model can have both <tt>:except</tt> and <tt>:only</tt> set. So if # <tt>:only</tt> is set, always delete <tt>:except</tt>. - def serializable_attribute_names - attribute_names = @serializable.attributes.keys.sort - + def serializable_attributes_hash + attributes = @serializable.attributes if options[:only].any? - attribute_names &= options[:only] + attributes.slice(*options[:only]) elsif options[:except].any? - attribute_names -= options[:except] + attributes.except(*options[:except]) + else + attributes end - - attribute_names end def serializable_attributes - serializable_attribute_names.collect { |name| Attribute.new(name, @serializable) } + serializable_attributes_hash.map { |name, value| self.class::Attribute.new(name, @serializable, value) } end def serializable_method_attributes Array.wrap(options[:methods]).inject([]) do |methods, name| - methods << MethodAttribute.new(name.to_s, @serializable) if @serializable.respond_to?(name.to_s) + methods << self.class::MethodAttribute.new(name.to_s, @serializable) if @serializable.respond_to?(name.to_s) methods end end diff --git a/activemodel/lib/active_model/validations/numericality.rb b/activemodel/lib/active_model/validations/numericality.rb index c6d84c5312..f974999bef 100644 --- a/activemodel/lib/active_model/validations/numericality.rb +++ b/activemodel/lib/active_model/validations/numericality.rb @@ -25,11 +25,18 @@ module ActiveModel return if options[:allow_nil] && raw_value.nil? - unless value = parse_raw_value(raw_value, options) + unless value = parse_raw_value_as_a_number(raw_value) record.errors.add(attr_name, :not_a_number, :value => raw_value, :default => options[:message]) return end + if options[:only_integer] + unless value = parse_raw_value_as_an_integer(raw_value) + record.errors.add(attr_name, :not_an_integer, :value => raw_value, :default => options[:message]) + return + end + end + options.slice(*CHECKS.keys).each do |option, option_value| case option when :odd, :even @@ -44,23 +51,23 @@ module ActiveModel record.errors.add(attr_name, option, :default => options[:message], :value => value, :count => option_value) end end - end + end end protected - def parse_raw_value(raw_value, options) - if options[:only_integer] - raw_value.to_i if raw_value.to_s =~ /\A[+-]?\d+\Z/ - else - begin - Kernel.Float(raw_value) - rescue ArgumentError, TypeError - nil - end + def parse_raw_value_as_a_number(raw_value) + begin + Kernel.Float(raw_value) + rescue ArgumentError, TypeError + nil end end + def parse_raw_value_as_an_integer(raw_value) + raw_value.to_i if raw_value.to_s =~ /\A[+-]?\d+\Z/ + end + end module ClassMethods diff --git a/activemodel/test/cases/validations/i18n_validation_test.rb b/activemodel/test/cases/validations/i18n_validation_test.rb index 7dd82e711d..1b4c1699ae 100644 --- a/activemodel/test/cases/validations/i18n_validation_test.rb +++ b/activemodel/test/cases/validations/i18n_validation_test.rb @@ -217,15 +217,15 @@ class I18nValidationTest < ActiveModel::TestCase def test_validates_numericality_of_only_integer_generates_message Person.validates_numericality_of :title, :only_integer => true - @person.title = 'a' - @person.errors.expects(:generate_message).with(:title, :not_a_number, {:value => 'a', :default => nil}) + @person.title = '0.0' + @person.errors.expects(:generate_message).with(:title, :not_an_integer, {:value => '0.0', :default => nil}) @person.valid? end def test_validates_numericality_of_only_integer_generates_message_with_custom_default_message Person.validates_numericality_of :title, :only_integer => true, :message => 'custom' - @person.title = 'a' - @person.errors.expects(:generate_message).with(:title, :not_a_number, {:value => 'a', :default => 'custom'}) + @person.title = '0.0' + @person.errors.expects(:generate_message).with(:title, :not_an_integer, {:value => '0.0', :default => 'custom'}) @person.valid? end @@ -441,20 +441,20 @@ class I18nValidationTest < ActiveModel::TestCase # validates_numericality_of with :only_integer w/o mocha def test_validates_numericality_of_only_integer_finds_custom_model_key_translation - I18n.backend.store_translations 'en', :activemodel => {:errors => {:models => {:person => {:attributes => {:title => {:not_a_number => 'custom message'}}}}}} - I18n.backend.store_translations 'en', :errors => {:messages => {:not_a_number => 'global message'}} + I18n.backend.store_translations 'en', :activemodel => {:errors => {:models => {:person => {:attributes => {:title => {:not_an_integer => 'custom message'}}}}}} + I18n.backend.store_translations 'en', :errors => {:messages => {:not_an_integer => 'global message'}} Person.validates_numericality_of :title, :only_integer => true - @person.title = 'a' + @person.title = '1.0' @person.valid? assert_equal ['custom message'], @person.errors[:title] end def test_validates_numericality_of_only_integer_finds_global_default_translation - I18n.backend.store_translations 'en', :errors => {:messages => {:not_a_number => 'global message'}} + I18n.backend.store_translations 'en', :errors => {:messages => {:not_an_integer => 'global message'}} Person.validates_numericality_of :title, :only_integer => true - @person.title = 'a' + @person.title = '1.0' @person.valid? assert_equal ['global message'], @person.errors[:title] end diff --git a/activerecord/CHANGELOG b/activerecord/CHANGELOG index c0c4df5035..fcb0e31f79 100644 --- a/activerecord/CHANGELOG +++ b/activerecord/CHANGELOG @@ -1,5 +1,9 @@ *Rails 3.0.0 [beta 4/release candidate] (unreleased)* +* Destroy uses optimistic locking. If lock_version on the record you're destroying doesn't match lock_version in the database, a StaleObjectError is raised. #1966 [Curtis Hawthorne] + +* PostgreSQL: drop support for old postgres driver. Use pg 0.9.0 or later. [Jeremy Kemper] + * Observers can prevent records from saving by returning false, just like before_save and friends. #4087 [Mislav Marohnić] diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index d94cc03938..6dbee9f4bf 100755 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -1500,7 +1500,16 @@ module ActiveRecord when :destroy method_name = "has_many_dependent_destroy_for_#{reflection.name}".to_sym define_method(method_name) do - send(reflection.name).each { |o| o.destroy } + send(reflection.name).each do |o| + # No point in executing the counter update since we're going to destroy the parent anyway + counter_method = ('belongs_to_counter_cache_before_destroy_for_' + self.class.name.downcase).to_sym + if(o.respond_to? counter_method) then + class << o + self + end.send(:define_method, counter_method, Proc.new {}) + end + o.destroy + end end before_destroy method_name when :delete_all diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index fd24dcddc3..2d7cfad80d 100755 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -1844,8 +1844,7 @@ module ActiveRecord #:nodoc: # user.is_admin? # => true def attributes=(new_attributes, guard_protected_attributes = true) return if new_attributes.nil? - attributes = new_attributes.dup - attributes.stringify_keys! + attributes = new_attributes.stringify_keys multi_parameter_attributes = [] attributes = remove_attributes_protected_from_mass_assignment(attributes) if guard_protected_attributes diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb index 8649f96498..d7b5bf8e31 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb @@ -13,12 +13,12 @@ module ActiveRecord when String, ActiveSupport::Multibyte::Chars value = value.to_s if column && column.type == :binary && column.class.respond_to?(:string_to_binary) - "#{quoted_string_prefix}'#{quote_string(column.class.string_to_binary(value))}'" # ' (for ruby-mode) + "'#{quote_string(column.class.string_to_binary(value))}'" # ' (for ruby-mode) elsif column && [:integer, :float].include?(column.type) value = column.type == :integer ? value.to_i : value.to_f value.to_s else - "#{quoted_string_prefix}'#{quote_string(value)}'" # ' (for ruby-mode) + "'#{quote_string(value)}'" # ' (for ruby-mode) end when NilClass then "NULL" when TrueClass then (column && column.type == :integer ? '1' : quoted_true) @@ -30,7 +30,7 @@ module ActiveRecord if value.acts_like?(:date) || value.acts_like?(:time) "'#{quoted_date(value)}'" else - "#{quoted_string_prefix}'#{quote_string(value.to_yaml)}'" + "'#{quote_string(value.to_yaml)}'" end end end @@ -67,10 +67,6 @@ module ActiveRecord value end.to_s(:db) end - - def quoted_string_prefix - '' - end end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index ceb1adc9e0..74fed4ad62 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -2,26 +2,12 @@ require 'active_record/connection_adapters/abstract_adapter' require 'active_support/core_ext/kernel/requires' require 'active_support/core_ext/object/blank' -begin - require_library_or_gem 'pg' -rescue LoadError => e - begin - require_library_or_gem 'postgres' - class PGresult - alias_method :nfields, :num_fields unless self.method_defined?(:nfields) - alias_method :ntuples, :num_tuples unless self.method_defined?(:ntuples) - alias_method :ftype, :type unless self.method_defined?(:ftype) - alias_method :cmd_tuples, :cmdtuples unless self.method_defined?(:cmd_tuples) - end - rescue LoadError - raise e - end -end - module ActiveRecord class Base # Establishes a connection to the database that's used by all Active Record objects def self.postgresql_connection(config) # :nodoc: + require 'pg' + config = config.symbolize_keys host = config[:host] port = config[:port] || 5432 @@ -277,20 +263,12 @@ module ActiveRecord true end - # Does PostgreSQL support standard conforming strings? - def supports_standard_conforming_strings? - # Temporarily set the client message level above error to prevent unintentional - # error messages in the logs when working on a PostgreSQL database server that - # does not support standard conforming strings. - client_min_messages_old = client_min_messages - self.client_min_messages = 'panic' - - # postgres-pr does not raise an exception when client_min_messages is set higher - # than error and "SHOW standard_conforming_strings" fails, but returns an empty - # PGresult instead. - has_support = query('SHOW standard_conforming_strings')[0][0] rescue false - self.client_min_messages = client_min_messages_old - has_support + # Enable standard-conforming strings if available. + def set_standard_conforming_strings + old, self.client_min_messages = client_min_messages, 'panic' + execute('SET standard_conforming_strings = on') rescue nil + ensure + self.client_min_messages = old end def supports_insert_with_returning? @@ -314,85 +292,23 @@ module ActiveRecord # QUOTING ================================================== # Escapes binary strings for bytea input to the database. - def escape_bytea(original_value) - if @connection.respond_to?(:escape_bytea) - self.class.instance_eval do - define_method(:escape_bytea) do |value| - @connection.escape_bytea(value) if value - end - end - elsif PGconn.respond_to?(:escape_bytea) - self.class.instance_eval do - define_method(:escape_bytea) do |value| - PGconn.escape_bytea(value) if value - end - end - else - self.class.instance_eval do - define_method(:escape_bytea) do |value| - if value - result = '' - value.each_byte { |c| result << sprintf('\\\\%03o', c) } - result - end - end - end - end - escape_bytea(original_value) + def escape_bytea(value) + @connection.escape_bytea(value) if value end # Unescapes bytea output from a database to the binary string it represents. # NOTE: This is NOT an inverse of escape_bytea! This is only to be used # on escaped binary output from database drive. - def unescape_bytea(original_value) - # In each case, check if the value actually is escaped PostgreSQL bytea output - # or an unescaped Active Record attribute that was just written. - if PGconn.respond_to?(:unescape_bytea) - self.class.instance_eval do - define_method(:unescape_bytea) do |value| - if value =~ /\\\d{3}/ - PGconn.unescape_bytea(value) - else - value - end - end - end - else - self.class.instance_eval do - define_method(:unescape_bytea) do |value| - if value =~ /\\\d{3}/ - result = '' - i, max = 0, value.size - while i < max - char = value[i] - if char == ?\\ - if value[i+1] == ?\\ - char = ?\\ - i += 1 - else - char = value[i+1..i+3].oct - i += 3 - end - end - result << char - i += 1 - end - result - else - value - end - end - end - end - unescape_bytea(original_value) + def unescape_bytea(value) + @connection.unescape_bytea(value) if value end # Quotes PostgreSQL-specific data types for SQL input. def quote(value, column = nil) #:nodoc: if value.kind_of?(String) && column && column.type == :binary - "#{quoted_string_prefix}'#{escape_bytea(value)}'" + "'#{escape_bytea(value)}'" elsif value.kind_of?(String) && column && column.sql_type == 'xml' - "xml E'#{quote_string(value)}'" + "xml '#{quote_string(value)}'" elsif value.kind_of?(Numeric) && column && column.sql_type == 'money' # Not truly string input, so doesn't require (or allow) escape string syntax. "'#{value.to_s}'" @@ -408,28 +324,9 @@ module ActiveRecord end end - # Quotes strings for use in SQL input in the postgres driver for better performance. - def quote_string(original_value) #:nodoc: - if @connection.respond_to?(:escape) - self.class.instance_eval do - define_method(:quote_string) do |s| - @connection.escape(s) - end - end - elsif PGconn.respond_to?(:escape) - self.class.instance_eval do - define_method(:quote_string) do |s| - PGconn.escape(s) - end - end - else - # There are some incorrectly compiled postgres drivers out there - # that don't define PGconn.escape. - self.class.instance_eval do - remove_method(:quote_string) - end - end - quote_string(original_value) + # Quotes strings for use in SQL input. + def quote_string(s) #:nodoc: + @connection.escape(s) end # Checks the following cases: @@ -1005,22 +902,11 @@ module ActiveRecord # Ignore async_exec and async_query when using postgres-pr. @async = @config[:allow_concurrency] && @connection.respond_to?(:async_exec) - # Use escape string syntax if available. We cannot do this lazily when encountering - # the first string, because that could then break any transactions in progress. - # See: http://www.postgresql.org/docs/current/static/runtime-config-compatible.html - # If PostgreSQL doesn't know the standard_conforming_strings parameter then it doesn't - # support escape string syntax. Don't override the inherited quoted_string_prefix. - if supports_standard_conforming_strings? - self.class.instance_eval do - define_method(:quoted_string_prefix) { 'E' } - end - end - # Money type has a fixed precision of 10 in PostgreSQL 8.2 and below, and as of # PostgreSQL 8.3 it has a fixed precision of 19. PostgreSQLColumn.extract_precision # should know about this but can't detect it there, so deal with it here. - PostgreSQLColumn.money_precision = - (postgresql_version >= 80300) ? 19 : 10 + PostgreSQLColumn.money_precision = (postgresql_version >= 80300) ? 19 : 10 + configure_connection end @@ -1036,7 +922,10 @@ module ActiveRecord end self.client_min_messages = @config[:min_messages] if @config[:min_messages] self.schema_search_path = @config[:schema_search_path] || @config[:schema_order] - + + # Use standard-conforming strings if available so we don't have to do the E'...' dance. + set_standard_conforming_strings + # If using ActiveRecord's time zone support configure the connection to return # TIMESTAMP WITH ZONE types in UTC. execute("SET time zone 'UTC'") if ActiveRecord::Base.default_timezone == :utc diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index 22f0e60083..0bc49c1daa 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -3,8 +3,9 @@ require 'yaml' require 'csv' require 'zlib' require 'active_support/dependencies' -require 'active_support/core_ext/logger' +require 'active_support/core_ext/array/wrap' require 'active_support/core_ext/object/blank' +require 'active_support/core_ext/logger' if RUBY_VERSION < '1.9' module YAML #:nodoc: @@ -492,6 +493,7 @@ class Fixtures < (RUBY_VERSION < '1.9' ? YAML::Omap : Hash) def self.create_fixtures(fixtures_directory, table_names, class_names = {}) table_names = [table_names].flatten.map { |n| n.to_s } + table_names.each { |n| class_names[n.tr('/', '_').to_sym] = n.classify if n.include?('/') } connection = block_given? ? yield : ActiveRecord::Base.connection table_names_to_fetch = table_names.reject { |table_name| fixture_is_cached?(connection, table_name) } @@ -502,7 +504,7 @@ class Fixtures < (RUBY_VERSION < '1.9' ? YAML::Omap : Hash) fixtures_map = {} fixtures = table_names_to_fetch.map do |table_name| - fixtures_map[table_name] = Fixtures.new(connection, File.split(table_name.to_s).last, class_names[table_name.to_sym], File.join(fixtures_directory, table_name.to_s)) + fixtures_map[table_name] = Fixtures.new(connection, table_name.tr('/', '_'), class_names[table_name.tr('/', '_').to_sym], File.join(fixtures_directory, table_name)) end all_loaded_fixtures.update(fixtures_map) @@ -836,8 +838,8 @@ module ActiveRecord def fixtures(*table_names) if table_names.first == :all - table_names = Dir["#{fixture_path}/*.yml"] + Dir["#{fixture_path}/*.csv"] - table_names.map! { |f| File.basename(f).split('.')[0..-2].join('.') } + table_names = Dir["#{fixture_path}/**/*.{yml,csv}"] + table_names.map! { |f| f[(fixture_path.size + 1)..-5] } else table_names = table_names.flatten.map { |n| n.to_s } end @@ -868,9 +870,9 @@ module ActiveRecord end def setup_fixture_accessors(table_names = nil) - table_names = [table_names] if table_names && !table_names.respond_to?(:each) - (table_names || fixture_table_names).each do |table_name| - table_name = table_name.to_s.tr('.', '_') + table_names = Array.wrap(table_names || fixture_table_names) + table_names.each do |table_name| + table_name = table_name.to_s.tr('./', '_') define_method(table_name) do |*fixtures| force_reload = fixtures.pop if fixtures.last == true || fixtures.last == :reload diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb index 9044ca418b..60ad23f38c 100644 --- a/activerecord/lib/active_record/locking/optimistic.rb +++ b/activerecord/lib/active_record/locking/optimistic.rb @@ -23,6 +23,16 @@ module ActiveRecord # p2.first_name = "should fail" # p2.save # Raises a ActiveRecord::StaleObjectError # + # Optimistic locking will also check for stale data when objects are destroyed. Example: + # + # p1 = Person.find(1) + # p2 = Person.find(1) + # + # p1.first_name = "Michael" + # p1.save + # + # p2.destroy # Raises a ActiveRecord::StaleObjectError + # # You're then responsible for dealing with the conflict by rescuing the exception and either rolling back, merging, # or otherwise apply the business logic needed to resolve the conflict. # @@ -39,6 +49,7 @@ module ActiveRecord self.lock_optimistically = true alias_method_chain :update, :lock + alias_method_chain :destroy, :lock alias_method_chain :attributes_from_column_definition, :lock class << self @@ -88,7 +99,7 @@ module ActiveRecord unless affected_rows == 1 - raise ActiveRecord::StaleObjectError, "Attempted to update a stale object" + raise ActiveRecord::StaleObjectError, "Attempted to update a stale object: #{self.class.name}" end affected_rows @@ -100,6 +111,28 @@ module ActiveRecord end end + def destroy_with_lock #:nodoc: + return destroy_without_lock unless locking_enabled? + + unless new_record? + lock_col = self.class.locking_column + previous_value = send(lock_col).to_i + + affected_rows = connection.delete( + "DELETE FROM #{self.class.quoted_table_name} " + + "WHERE #{connection.quote_column_name(self.class.primary_key)} = #{quoted_id} " + + "AND #{self.class.quoted_locking_column} = #{quote_value(previous_value)}", + "#{self.class.name} Destroy" + ) + + unless affected_rows == 1 + raise ActiveRecord::StaleObjectError, "Attempted to delete a stale object: #{self.class.name}" + end + end + + freeze + end + module ClassMethods DEFAULT_LOCKING_COLUMN = 'lock_version' diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb index 70a460d41d..6718b4a69d 100644 --- a/activerecord/lib/active_record/nested_attributes.rb +++ b/activerecord/lib/active_record/nested_attributes.rb @@ -1,6 +1,7 @@ require 'active_support/core_ext/hash/except' require 'active_support/core_ext/object/try' require 'active_support/core_ext/object/blank' +require 'active_support/core_ext/hash/indifferent_access' module ActiveRecord module NestedAttributes #:nodoc: diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb index 878a4dac09..f3d21d4969 100644 --- a/activerecord/lib/active_record/railtie.rb +++ b/activerecord/lib/active_record/railtie.rb @@ -70,27 +70,23 @@ module ActiveRecord end end - initializer "active_record.add_observer_hook", :after=>"active_record.set_configs" do |app| + initializer "active_record.set_dispatch_hooks", :before => :set_clear_dependencies_hook do |app| ActiveSupport.on_load(:active_record) do - ActionDispatch::Callbacks.to_prepare(:activerecord_instantiate_observers) do - ActiveRecord::Base.instantiate_observers + unless app.config.cache_classes + ActionDispatch::Callbacks.after do + ActiveRecord::Base.reset_subclasses + ActiveRecord::Base.clear_reloadable_connections! + end end end end - initializer "active_record.instantiate_observers", :after=>"active_record.initialize_database" do + config.after_initialize do ActiveSupport.on_load(:active_record) do instantiate_observers - end - end - initializer "active_record.set_dispatch_hooks", :before => :set_clear_dependencies_hook do |app| - ActiveSupport.on_load(:active_record) do - unless app.config.cache_classes - ActionDispatch::Callbacks.after do - ActiveRecord::Base.reset_subclasses - ActiveRecord::Base.clear_reloadable_connections! - end + ActionDispatch::Callbacks.to_prepare(:activerecord_instantiate_observers) do + ActiveRecord::Base.instantiate_observers end end end diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index 2b53afc68b..cb7eade0ab 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -258,8 +258,8 @@ namespace :db do base_dir = ENV['FIXTURES_PATH'] ? File.join(Rails.root, ENV['FIXTURES_PATH']) : File.join(Rails.root, 'test', 'fixtures') fixtures_dir = ENV['FIXTURES_DIR'] ? File.join(base_dir, ENV['FIXTURES_DIR']) : base_dir - (ENV['FIXTURES'] ? ENV['FIXTURES'].split(/,/).map {|f| File.join(fixtures_dir, f) } : Dir.glob(File.join(fixtures_dir, '*.{yml,csv}'))).each do |fixture_file| - Fixtures.create_fixtures(File.dirname(fixture_file), File.basename(fixture_file, '.*')) + (ENV['FIXTURES'] ? ENV['FIXTURES'].split(/,/).map {|f| File.join(fixtures_dir, f) } : Dir["#{fixtures_dir}/**/*.{yml,csv}"]).each do |fixture_file| + Fixtures.create_fixtures(fixtures_dir, fixture_file[(fixtures_dir.size + 1)..-5]) end end diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index a26f1c0ac8..3514d0a259 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -1,4 +1,5 @@ require 'active_support/core_ext/object/blank' +require 'active_support/core_ext/hash/indifferent_access' module ActiveRecord module FinderMethods diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 09332418d5..58af930446 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -187,15 +187,13 @@ module ActiveRecord def build_where(*args) return if args.blank? - builder = PredicateBuilder.new(table.engine) - opts = args.first case opts when String, Array @klass.send(:sanitize_sql, args.size > 1 ? args : opts) when Hash attributes = @klass.send(:expand_hash_conditions_for_aggregates, opts) - builder.build_from_hash(attributes, table) + PredicateBuilder.new(table.engine).build_from_hash(attributes, table) else opts end diff --git a/activerecord/lib/active_record/serializers/xml_serializer.rb b/activerecord/lib/active_record/serializers/xml_serializer.rb index 2e85959b1e..255b03433d 100644 --- a/activerecord/lib/active_record/serializers/xml_serializer.rb +++ b/activerecord/lib/active_record/serializers/xml_serializer.rb @@ -182,17 +182,6 @@ module ActiveRecord #:nodoc: options[:except] |= Array.wrap(@serializable.class.inheritance_column) end - def serializable_attributes - serializable_attribute_names.collect { |name| Attribute.new(name, @serializable) } - end - - def serializable_method_attributes - Array.wrap(options[:methods]).inject([]) do |method_attributes, name| - method_attributes << MethodAttribute.new(name.to_s, @serializable) if @serializable.respond_to?(name.to_s) - method_attributes - end - end - def add_associations(association, records, opts) if records.is_a?(Enumerable) tag = reformat_name(association.to_s) diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb index 9e88ec8016..77b2b748b1 100644 --- a/activerecord/test/cases/finder_test.rb +++ b/activerecord/test/cases/finder_test.rb @@ -566,8 +566,8 @@ class FinderTest < ActiveRecord::TestCase end def test_string_sanitation - assert_not_equal "#{ActiveRecord::Base.connection.quoted_string_prefix}'something ' 1=1'", ActiveRecord::Base.sanitize("something ' 1=1") - assert_equal "#{ActiveRecord::Base.connection.quoted_string_prefix}'something; select table'", ActiveRecord::Base.sanitize("something; select table") + assert_not_equal "'something ' 1=1'", ActiveRecord::Base.sanitize("something ' 1=1") + assert_equal "'something; select table'", ActiveRecord::Base.sanitize("something; select table") end def test_count diff --git a/activerecord/test/cases/fixtures_test.rb b/activerecord/test/cases/fixtures_test.rb index d24283fe4e..3ce23209cc 100644 --- a/activerecord/test/cases/fixtures_test.rb +++ b/activerecord/test/cases/fixtures_test.rb @@ -16,6 +16,9 @@ require 'models/treasure' require 'models/matey' require 'models/ship' require 'models/book' +require 'models/admin' +require 'models/admin/account' +require 'models/admin/user' class FixturesTest < ActiveRecord::TestCase self.use_instantiated_fixtures = true @@ -507,7 +510,7 @@ class FasterFixturesTest < ActiveRecord::TestCase end class FoxyFixturesTest < ActiveRecord::TestCase - fixtures :parrots, :parrots_pirates, :pirates, :treasures, :mateys, :ships, :computers, :developers + fixtures :parrots, :parrots_pirates, :pirates, :treasures, :mateys, :ships, :computers, :developers, :"admin/accounts", :"admin/users" def test_identifies_strings assert_equal(Fixtures.identify("foo"), Fixtures.identify("foo")) @@ -629,6 +632,11 @@ class FoxyFixturesTest < ActiveRecord::TestCase assert_kind_of DeadParrot, parrots(:polly) assert_equal pirates(:blackbeard), parrots(:polly).killer end + + def test_namespaced_models + assert admin_accounts(:signals37).users.include?(admin_users(:david)) + assert_equal 2, admin_accounts(:signals37).users.size + end end class ActiveSupportSubclassWithFixturesTest < ActiveRecord::TestCase diff --git a/activerecord/test/cases/locking_test.rb b/activerecord/test/cases/locking_test.rb index dfaecf35cf..aa2d9527f9 100644 --- a/activerecord/test/cases/locking_test.rb +++ b/activerecord/test/cases/locking_test.rb @@ -38,6 +38,25 @@ class OptimisticLockingTest < ActiveRecord::TestCase assert_raise(ActiveRecord::StaleObjectError) { p2.save! } end + # See Lighthouse ticket #1966 + def test_lock_destroy + p1 = Person.find(1) + p2 = Person.find(1) + assert_equal 0, p1.lock_version + assert_equal 0, p2.lock_version + + p1.first_name = 'stu' + p1.save! + assert_equal 1, p1.lock_version + assert_equal 0, p2.lock_version + + assert_raises(ActiveRecord::StaleObjectError) { p2.destroy } + + assert p1.destroy + assert_equal true, p1.frozen? + assert_raises(ActiveRecord::RecordNotFound) { Person.find(1) } + end + def test_lock_repeating p1 = Person.find(1) p2 = Person.find(1) @@ -150,6 +169,32 @@ class OptimisticLockingTest < ActiveRecord::TestCase end end end + + # See Lighthouse ticket #1966 + def test_destroy_dependents + # Establish dependent relationship between People and LegacyThing + add_counter_column_to(Person, 'legacy_things_count') + LegacyThing.connection.add_column LegacyThing.table_name, 'person_id', :integer + LegacyThing.reset_column_information + LegacyThing.class_eval do + belongs_to :person, :counter_cache => true + end + Person.class_eval do + has_many :legacy_things, :dependent => :destroy + end + + # Make sure that counter incrementing doesn't cause problems + p1 = Person.new(:first_name => 'fjord') + p1.save! + t = LegacyThing.new(:person => p1) + t.save! + p1.reload + assert_equal 1, p1.legacy_things_count + assert p1.destroy + assert_equal true, p1.frozen? + assert_raises(ActiveRecord::RecordNotFound) { Person.find(p1.id) } + assert_raises(ActiveRecord::RecordNotFound) { LegacyThing.find(t.id) } + end def test_quote_table_name ref = references(:michael_magician) @@ -168,11 +213,11 @@ class OptimisticLockingTest < ActiveRecord::TestCase private - def add_counter_column_to(model) - model.connection.add_column model.table_name, :test_count, :integer, :null => false, :default => 0 + def add_counter_column_to(model, col='test_count') + model.connection.add_column model.table_name, col, :integer, :null => false, :default => 0 model.reset_column_information # OpenBase does not set a value to existing rows when adding a not null default column - model.update_all(:test_count => 0) if current_adapter?(:OpenBaseAdapter) + model.update_all(col => 0) if current_adapter?(:OpenBaseAdapter) end def remove_counter_column_from(model) diff --git a/activerecord/test/fixtures/admin/accounts.yml b/activerecord/test/fixtures/admin/accounts.yml new file mode 100644 index 0000000000..9e341a15af --- /dev/null +++ b/activerecord/test/fixtures/admin/accounts.yml @@ -0,0 +1,2 @@ +signals37: + name: 37signals diff --git a/activerecord/test/fixtures/admin/users.yml b/activerecord/test/fixtures/admin/users.yml new file mode 100644 index 0000000000..6f11f2509e --- /dev/null +++ b/activerecord/test/fixtures/admin/users.yml @@ -0,0 +1,7 @@ +david: + name: David + account: signals37 + +jamis: + name: Jamis + account: signals37 diff --git a/activerecord/test/models/admin.rb b/activerecord/test/models/admin.rb new file mode 100644 index 0000000000..00e69fbed8 --- /dev/null +++ b/activerecord/test/models/admin.rb @@ -0,0 +1,5 @@ +module Admin + def self.table_name_prefix + 'admin_' + end +end
\ No newline at end of file diff --git a/activerecord/test/models/admin/account.rb b/activerecord/test/models/admin/account.rb new file mode 100644 index 0000000000..46de28aae1 --- /dev/null +++ b/activerecord/test/models/admin/account.rb @@ -0,0 +1,3 @@ +class Admin::Account < ActiveRecord::Base + has_many :users +end
\ No newline at end of file diff --git a/activerecord/test/models/admin/user.rb b/activerecord/test/models/admin/user.rb new file mode 100644 index 0000000000..74bb21551e --- /dev/null +++ b/activerecord/test/models/admin/user.rb @@ -0,0 +1,3 @@ +class Admin::User < ActiveRecord::Base + belongs_to :account +end
\ No newline at end of file diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index 7a0cf550e0..f5fba2f87d 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -26,6 +26,15 @@ ActiveRecord::Schema.define do t.integer :credit_limit end + create_table :admin_accounts, :force => true do |t| + t.string :name + end + + create_table :admin_users, :force => true do |t| + t.string :name + t.references :account + end + create_table :audit_logs, :force => true do |t| t.column :message, :string, :null=>false t.column :developer_id, :integer, :null=>false diff --git a/activeresource/CHANGELOG b/activeresource/CHANGELOG index 91dccb9671..bd97b2d549 100644 --- a/activeresource/CHANGELOG +++ b/activeresource/CHANGELOG @@ -1,3 +1,8 @@ +*Rails 3.0.0 [beta 4/release candidate] (unreleased)* + +* JSON: set Base.include_root_in_json = true to include a root value in the JSON: {"post": {"title": ...}}. Mirrors the Active Record option. [Santiago Pastorino] + + *Rails 3.0.0 [beta 3] (April 13th, 2010)* * No changes diff --git a/activeresource/lib/active_resource/base.rb b/activeresource/lib/active_resource/base.rb index 1dd5af8098..ad994214f6 100644 --- a/activeresource/lib/active_resource/base.rb +++ b/activeresource/lib/active_resource/base.rb @@ -251,9 +251,6 @@ module ActiveResource # The logger for diagnosing and tracing Active Resource calls. cattr_accessor :logger - # Controls the top-level behavior of JSON serialization - cattr_accessor :include_root_in_json, :instance_writer => false - class << self # Creates a schema for this resource - setting the attributes that are # known prior to fetching an instance from the remote system. @@ -1179,79 +1176,11 @@ module ActiveResource !new? && self.class.exists?(to_param, :params => prefix_options) end - # Converts the resource to an XML string representation. - # - # ==== Options - # The +options+ parameter is handed off to the +to_xml+ method on each - # attribute, so it has the same options as the +to_xml+ methods in - # Active Support. - # - # * <tt>:indent</tt> - Set the indent level for the XML output (default is +2+). - # * <tt>:dasherize</tt> - Boolean option to determine whether or not element names should - # replace underscores with dashes (default is <tt>false</tt>). - # * <tt>:skip_instruct</tt> - Toggle skipping the +instruct!+ call on the XML builder - # that generates the XML declaration (default is <tt>false</tt>). - # - # ==== Examples - # my_group = SubsidiaryGroup.find(:first) - # my_group.to_xml - # # => <?xml version="1.0" encoding="UTF-8"?> - # # <subsidiary_group> [...] </subsidiary_group> - # - # my_group.to_xml(:dasherize => true) - # # => <?xml version="1.0" encoding="UTF-8"?> - # # <subsidiary-group> [...] </subsidiary-group> - # - # my_group.to_xml(:skip_instruct => true) - # # => <subsidiary_group> [...] </subsidiary_group> - def to_xml(options={}) - attributes.to_xml({:root => self.class.element_name}.merge(options)) - end - - # Coerces to a hash for JSON encoding. - # - # ==== Options - # The +options+ are passed to the +to_json+ method on each - # attribute, so the same options as the +to_json+ methods in - # Active Support. - # - # * <tt>:only</tt> - Only include the specified attribute or list of - # attributes in the serialized output. Attribute names must be specified - # as strings. - # * <tt>:except</tt> - Do not include the specified attribute or list of - # attributes in the serialized output. Attribute names must be specified - # as strings. - # - # ==== Examples - # person = Person.new(:first_name => "Jim", :last_name => "Smith") - # person.to_json - # # => {"first_name": "Jim", "last_name": "Smith"} - # - # person.to_json(:only => ["first_name"]) - # # => {"first_name": "Jim"} - # - # person.to_json(:except => ["first_name"]) - # # => {"last_name": "Smith"} - def as_json(options = nil) - attributes.as_json(options) - end - # Returns the serialized string representation of the resource in the configured # serialization format specified in ActiveResource::Base.format. The options # applicable depend on the configured encoding format. def encode(options={}) - case self.class.format - when ActiveResource::Formats::XmlFormat - self.class.format.encode(attributes, {:root => self.class.element_name}.merge(options)) - when ActiveResource::Formats::JsonFormat - if ActiveResource::Base.include_root_in_json - self.class.format.encode({self.class.element_name => attributes}, options) - else - self.class.format.encode(attributes, options) - end - else - self.class.format.encode(attributes, options) - end + send("to_#{self.class.format.extension}", options) end # A method to \reload the attributes of this object from the remote web service. @@ -1475,5 +1404,7 @@ module ActiveResource class Base extend ActiveModel::Naming include CustomMethods, Observing, Validations + include ActiveModel::Serializers::JSON + include ActiveModel::Serializers::Xml end end diff --git a/activeresource/test/cases/base_test.rb b/activeresource/test/cases/base_test.rb index 31e0dc0abc..2ed7e1c95f 100644 --- a/activeresource/test/cases/base_test.rb +++ b/activeresource/test/cases/base_test.rb @@ -1015,14 +1015,17 @@ class BaseTest < Test::Unit::TestCase assert xml.include?('<id type="integer">1</id>') end - - def test_to_json_including_root + def test_to_json Person.include_root_in_json = true Person.format = :json joe = Person.find(6) json = joe.encode Person.format = :xml - assert_equal json, '{"person":{"person":{"name":"Joe","id":6}}}' + + assert_match %r{^\{"person":\{"person":\{}, json + assert_match %r{"id":6}, json + assert_match %r{"name":"Joe"}, json + assert_match %r{\}\}\}$}, json end def test_to_param_quacks_like_active_record diff --git a/activesupport/CHANGELOG b/activesupport/CHANGELOG index da370303c6..7bfc377ff1 100644 --- a/activesupport/CHANGELOG +++ b/activesupport/CHANGELOG @@ -1,5 +1,44 @@ +*Rails 3.0.0 [beta 4/release candidate] (unreleased)* + +* Harmonize the caching API and refactor the backends. #4452 [Brian Durand] + All caches: + * Add default options to initializer that will be sent to all read, write, fetch, exist?, increment, and decrement + * Add support for the :expires_in option to fetch and write for all caches. Cache entries are stored with the create timestamp and a ttl so that expiration can be handled independently of the implementation. + * Add support for a :namespace option. This can be used to set a global prefix for cache entries. + * Deprecate expand_cache_key on ActiveSupport::Cache and move it to ActionController::Caching and ActionDispatch::Http::Cache since the logic in the method used some Rails specific environment variables and was only used by ActionPack classes. Not very DRY but there didn't seem to be a good shared spot and ActiveSupport really shouldn't be Rails specific. + * Add support for :race_condition_ttl to fetch. This setting can prevent race conditions on fetch calls where several processes try to regenerate a recently expired entry at once. + * Add support for :compress option to fetch and write which will compress any data over a configurable threshold. + * Nil values can now be stored in the cache and are distinct from cache misses for fetch. + * Easier API to create new implementations. Just need to implement the methods read_entry, write_entry, and delete_entry instead of overwriting existing methods. + * Since all cache implementations support storing objects, update the docs to state that ActiveCache::Cache::Store implementations should store objects. Keys, however, must be strings since some implementations require that. + * Increase test coverage. + * Document methods which are provided as convenience but which may not be universally available. + + MemoryStore: + * MemoryStore can now safely be used as the cache for single server sites. + * Make thread safe so that the default cache implementation used by Rails is thread safe. The overhead is minimal and it is still the fastest store available. + * Provide :size initialization option indicating the maximum size of the cache in memory (defaults to 32Mb). + * Add prune logic that removes the least recently used cache entries to keep the cache size from exceeding the max. + * Deprecated SynchronizedMemoryStore since it isn't needed anymore. + + FileStore: + * Escape key values so they will work as file names on all file systems, be consistent, and case sensitive + * Use a hash algorithm to segment the cache into sub directories so that a large cache doesn't exceed file system limits. + * FileStore can be slow so implement the LocalCache strategy to cache reads for the duration of a request. + * Add cleanup method to keep the disk from filling up with expired entries. + * Fix increment and decrement to use file system locks so they are consistent between processes. + + MemCacheStore: + * Support all keys. Previously keys with spaces in them would fail + * Deprecate CompressedMemCacheStore since it isn't needed anymore (use :compress => true) + +* JSON: encode objects that don't have a native JSON representation using to_hash, if available, instead of instance_values (the old fallback) or to_s (other encoders' default). Encode BigDecimal and Regexp encode as strings to conform with other encoders. Try to transcode non-UTF-8 strings. [Jeremy Kemper] + + *Rails 3.0.0 [beta 3] (April 13th, 2010)* +* HashWithIndifferentAccess: remove inherited symbolize_keys! since its keys are always strings. [Santiago Pastorino] + * Improve transliteration quality. #4374 [Norman Clarke] * Speed up and add Ruby 1.9 support for ActiveSupport::Multibyte::Chars#tidy_bytes. #4350 [Norman Clarke] diff --git a/activesupport/lib/active_support/cache.rb b/activesupport/lib/active_support/cache.rb index 7213b24f2d..ec5007c284 100644 --- a/activesupport/lib/active_support/cache.rb +++ b/activesupport/lib/active_support/cache.rb @@ -1,8 +1,12 @@ require 'benchmark' +require 'zlib' +require 'active_support/core_ext/array/extract_options' require 'active_support/core_ext/array/wrap' require 'active_support/core_ext/benchmark' require 'active_support/core_ext/exception' require 'active_support/core_ext/class/attribute_accessors' +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' @@ -11,10 +15,16 @@ module ActiveSupport module Cache autoload :FileStore, 'active_support/cache/file_store' autoload :MemoryStore, 'active_support/cache/memory_store' - autoload :SynchronizedMemoryStore, 'active_support/cache/synchronized_memory_store' autoload :MemCacheStore, 'active_support/cache/mem_cache_store' + autoload :SynchronizedMemoryStore, 'active_support/cache/synchronized_memory_store' autoload :CompressedMemCacheStore, 'active_support/cache/compressed_mem_cache_store' + EMPTY_OPTIONS = {}.freeze + + # These options mean something to all cache implementations. Individual cache + # implementations may support additional optons. + UNIVERSAL_OPTIONS = [:namespace, :compress, :compress_threshold, :expires_in, :race_condition_ttl] + module Strategy autoload :LocalCache, 'active_support/cache/strategy/local_cache' end @@ -59,15 +69,12 @@ module ActiveSupport end end - RAILS_CACHE_ID = ENV["RAILS_CACHE_ID"] - RAILS_APP_VERION = ENV["RAILS_APP_VERION"] - EXPANDED_CACHE = RAILS_CACHE_ID || RAILS_APP_VERION - def self.expand_cache_key(key, namespace = nil) expanded_cache_key = namespace ? "#{namespace}/" : "" - if EXPANDED_CACHE - expanded_cache_key << "#{RAILS_CACHE_ID || RAILS_APP_VERION}/" + prefix = ENV["RAILS_CACHE_ID"] || ENV["RAILS_APP_VERSION"] + if prefix + expanded_cache_key << "#{prefix}/" end expanded_cache_key << @@ -92,26 +99,75 @@ module ActiveSupport # ActiveSupport::Cache::MemCacheStore. MemCacheStore is currently the most # popular cache store for large production websites. # - # ActiveSupport::Cache::Store is meant for caching strings. Some cache - # store implementations, like MemoryStore, are able to cache arbitrary - # Ruby objects, but don't count on every cache store to be able to do that. + # Some implementations may not support all methods beyond the basic cache + # methods of +fetch+, +write+, +read+, +exist?+, and +delete+. + # + # ActiveSupport::Cache::Store can store any serializable Ruby object. # # cache = ActiveSupport::Cache::MemoryStore.new # # cache.read("city") # => nil # cache.write("city", "Duckburgh") # cache.read("city") # => "Duckburgh" + # + # Keys are always translated into Strings and are case sensitive. When an + # object is specified as a key, its +cache_key+ method will be called if it + # is defined. Otherwise, the +to_param+ method will be called. Hashes and + # Arrays can be used as keys. The elements will be delimited by slashes + # and Hashes elements will be sorted by key so they are consistent. + # + # cache.read("city") == cache.read(:city) # => true + # + # Nil values can be cached. + # + # If your cache is on a shared infrastructure, you can define a namespace for + # your cache entries. If a namespace is defined, it will be prefixed on to every + # key. The namespace can be either a static value or a Proc. If it is a Proc, it + # will be invoked when each key is evaluated so that you can use application logic + # to invalidate keys. + # + # cache.namespace = lambda { @last_mod_time } # Set the namespace to a variable + # @last_mod_time = Time.now # Invalidate the entire cache by changing namespace + # + # All caches support auto expiring content after a specified number of seconds. + # To set the cache entry time to live, you can either specify +:expires_in+ as + # an option to the constructor to have it affect all entries or to the +fetch+ + # or +write+ methods for just one entry. + # + # cache = ActiveSupport::Cache::MemoryStore.new(:expire_in => 5.minutes) + # cache.write(key, value, :expire_in => 1.minute) # Set a lower value for one entry + # + # Caches can also store values in a compressed format to save space and reduce + # time spent sending data. Since there is some overhead, values must be large + # enough to warrant compression. To turn on compression either pass + # <tt>:compress => true</tt> in the initializer or to +fetch+ or +write+. + # To specify the threshold at which to compress values, set + # <tt>:compress_threshold</tt>. The default threshold is 32K. class Store - cattr_accessor :logger, :instance_writter => false + + cattr_accessor :logger, :instance_writer => true attr_reader :silence alias :silence? :silence + # Create a new cache. The options will be passed to any write method calls except + # for :namespace which can be used to set the global namespace for the cache. + def initialize (options = nil) + @options = options ? options.dup : {} + end + + # Get the default options set when the cache was created. + def options + @options ||= {} + end + + # Silence the logger. def silence! @silence = true self end + # Silence the logger within a block. def mute previous_silence, @silence = defined?(@silence) && @silence, true yield @@ -152,28 +208,85 @@ module ActiveSupport # cache.write("today", "Monday") # cache.fetch("today", :force => true) # => nil # + # Setting <tt>:compress</tt> will store a large cache entry set by the call + # in a compressed format. + # + # Setting <tt>:expires_in</tt> will set an expiration time on the cache + # entry if it is set by call. + # + # Setting <tt>:race_condition_ttl</tt> will invoke logic on entries set with + # an <tt>:expires_in</tt> option. If an entry is found in the cache that is + # expired and it has been expired for less than the number of seconds specified + # by this option and a block was passed to the method call, then the expiration + # future time of the entry in the cache will be updated to that many seconds + # in the and the block will be evaluated and written to the cache. + # + # This is very useful in situations where a cache entry is used very frequently + # under heavy load. The first process to find an expired cache entry will then + # become responsible for regenerating that entry while other processes continue + # to use the slightly out of date entry. This can prevent race conditions where + # too many processes are trying to regenerate the entry all at once. If the + # process regenerating the entry errors out, the entry will be regenerated + # after the specified number of seconds. + # + # # Set all values to expire after one minute. + # cache = ActiveSupport::Cache::MemoryCache.new(:expires_in => 1.minute) + # + # cache.write("foo", "original value") + # val_1 = nil + # val_2 = nil + # sleep 60 + # + # Thread.new do + # val_1 = cache.fetch("foo", :race_condition_ttl => 10) do + # sleep 1 + # "new value 1" + # end + # end + # + # Thread.new do + # val_2 = cache.fetch("foo", :race_condition_ttl => 10) do + # "new value 2" + # end + # end + # + # # val_1 => "new value 1" + # # val_2 => "original value" + # # cache.fetch("foo") => "new value 1" + # # Other options will be handled by the specific cache store implementation. - # Internally, #fetch calls #read, and calls #write on a cache miss. + # Internally, #fetch calls #read_entry, and calls #write_entry on a cache miss. # +options+ will be passed to the #read and #write calls. # - # For example, MemCacheStore's #write method supports the +:expires_in+ - # option, which tells the memcached server to automatically expire the - # cache item after a certain period. This options is also supported by - # FileStore's #read method. We can use this option with #fetch too: + # For example, MemCacheStore's #write method supports the +:raw+ + # option, which tells the memcached server to store all values as strings. + # We can use this option with #fetch too: # # cache = ActiveSupport::Cache::MemCacheStore.new - # cache.fetch("foo", :force => true, :expires_in => 5.seconds) do - # "bar" + # cache.fetch("foo", :force => true, :raw => true) do + # :bar # end # cache.fetch("foo") # => "bar" - # sleep(6) - # cache.fetch("foo") # => nil - def fetch(key, options = {}, &block) - if !options[:force] && value = read(key, options) - value + def fetch(name, options = nil, &block) + options = merged_options(options) + key = namespaced_key(name, options) + entry = instrument(:read, name, options) { read_entry(key, options) } unless options[:force] + if entry && entry.expired? + race_ttl = options[:race_condition_ttl].to_f + if race_ttl and Time.now.to_f - entry.expires_at <= race_ttl + entry.expires_at = Time.now + race_ttl + write_entry(key, entry, :expires_in => race_ttl * 2) + else + delete_entry(key, options) + end + entry = nil + end + + if entry + entry.value elsif block_given? - result = instrument(:generate, key, options, &block) - write(key, result, options) + result = instrument(:generate, name, options, &block) + write(name, result, options) result end end @@ -182,15 +295,47 @@ module ActiveSupport # the cache with the given key, then that data is returned. Otherwise, # nil is returned. # - # You may also specify additional options via the +options+ argument. - # The specific cache store implementation will decide what to do with - # +options+. + # Options are passed to the underlying cache implementation. + def read(name, options = nil) + options = merged_options(options) + key = namespaced_key(name, options) + instrument(:read, name, options) do + entry = read_entry(key, options) + if entry + if entry.expired? + delete_entry(key, options) + nil + else + entry.value + end + else + nil + end + end + end + + # Read multiple values at once from the cache. Options can be passed + # in the last argument. # - # For example, FileStore supports the +:expires_in+ option, which - # makes the method return nil for cache items older than the specified - # period. - def read(key, options = nil, &block) - instrument(:read, key, options, &block) + # Some cache implementation may optimize this method. + # + # Returns a hash mapping the names provided to the values found. + def read_multi(*names) + options = names.extract_options! + options = merged_options(options) + results = {} + names.each do |name| + key = namespaced_key(name, options) + entry = read_entry(key, options) + if entry + if entry.expired? + delete_entry(key) + else + results[name] = entry.value + end + end + end + results end # Writes the given value to the cache, with the given key. @@ -198,56 +343,160 @@ module ActiveSupport # You may also specify additional options via the +options+ argument. # The specific cache store implementation will decide what to do with # +options+. + def write(name, value, options = nil) + options = merged_options(options) + instrument(:write, name, options) do + entry = Entry.new(value, options) + write_entry(namespaced_key(name, options), entry, options) + end + end + + # Delete an entry in the cache. Returns +true+ if there was an entry to delete. # - # For example, MemCacheStore supports the +:expires_in+ option, which - # tells the memcached server to automatically expire the cache item after - # a certain period: + # Options are passed to the underlying cache implementation. + def delete(name, options = nil) + options = merged_options(options) + instrument(:delete, name) do + delete_entry(namespaced_key(name, options), options) + end + end + + # Return true if the cache contains an entry with this name. # - # cache = ActiveSupport::Cache::MemCacheStore.new - # cache.write("foo", "bar", :expires_in => 5.seconds) - # cache.read("foo") # => "bar" - # sleep(6) - # cache.read("foo") # => nil - def write(key, value, options = nil, &block) - instrument(:write, key, options, &block) + # Options are passed to the underlying cache implementation. + def exist?(name, options = nil) + options = merged_options(options) + instrument(:exist?, name) do + entry = read_entry(namespaced_key(name, options), options) + if entry && !entry.expired? + true + else + false + end + end end - def delete(key, options = nil, &block) - instrument(:delete, key, options, &block) + # Delete all entries whose keys match a pattern. + # + # Options are passed to the underlying cache implementation. + # + # Not all implementations may support +delete_matched+. + def delete_matched(matcher, options = nil) + raise NotImplementedError.new("#{self.class.name} does not support delete_matched") end - def delete_matched(matcher, options = nil, &block) - instrument(:delete_matched, matcher.inspect, options, &block) + # Increment an integer value in the cache. + # + # Options are passed to the underlying cache implementation. + # + # Not all implementations may support +delete_matched+. + def increment(name, amount = 1, options = nil) + raise NotImplementedError.new("#{self.class.name} does not support increment") end - def exist?(key, options = nil, &block) - instrument(:exist?, key, options, &block) + # Increment an integer value in the cache. + # + # Options are passed to the underlying cache implementation. + # + # Not all implementations may support +delete_matched+. + def decrement(name, amount = 1, options = nil) + raise NotImplementedError.new("#{self.class.name} does not support decrement") end - def increment(key, amount = 1) - if num = read(key) - write(key, num + amount) - else - nil - end + # Cleanup the cache by removing expired entries. Not all cache implementations may + # support this method. + # + # Options are passed to the underlying cache implementation. + # + # Not all implementations may support +delete_matched+. + def cleanup(options = nil) + raise NotImplementedError.new("#{self.class.name} does not support cleanup") end - def decrement(key, amount = 1) - if num = read(key) - write(key, num - amount) - else - nil - end + # Clear the entire cache. Not all cache implementations may support this method. + # You should be careful with this method since it could affect other processes + # if you are using a shared cache. + # + # Options are passed to the underlying cache implementation. + # + # Not all implementations may support +delete_matched+. + def clear(options = nil) + raise NotImplementedError.new("#{self.class.name} does not support clear") end + protected + # Add the namespace defined in the options to a pattern designed to match keys. + # Implementations that support delete_matched should call this method to translate + # a pattern that matches names into one that matches namespaced keys. + def key_matcher(pattern, options) + prefix = options[:namespace].is_a?(Proc) ? options[:namespace].call : options[:namespace] + if prefix + source = pattern.source + if source.start_with?('^') + source = source[1, source.length] + else + source = ".*#{source[0, source.length]}" + end + Regexp.new("^#{Regexp.escape(prefix)}:#{source}", pattern.options) + else + pattern + end + end + + # Read an entry from the cache implementation. Subclasses must implement this method. + def read_entry(key, options) # :nodoc: + raise NotImplementedError.new + end + + # Write an entry to the cache implementation. Subclasses must implement this method. + def write_entry(key, entry, options) # :nodoc: + raise NotImplementedError.new + end + + # Delete an entry from the cache implementation. Subclasses must implement this method. + def delete_entry(key, options) # :nodoc: + raise NotImplementedError.new + end + private - def expires_in(options) - expires_in = options && options[:expires_in] - raise ":expires_in must be a number" if expires_in && !expires_in.is_a?(Numeric) - expires_in || 0 + # Merge the default options with ones specific to a method call. + def merged_options(call_options) # :nodoc: + if call_options + options.merge(call_options) + else + options.dup + end + end + + # Expand a key to be a consistent string value. If the object responds to +cache_key+, + # it will be called. Otherwise, the to_param method will be called. If the key is a + # Hash, the keys will be sorted alphabetically. + def expanded_key(key) # :nodoc: + if key.respond_to?(:cache_key) + key = key.cache_key.to_s + elsif key.is_a?(Array) + if key.size > 1 + key.collect{|element| expanded_key(element)}.to_param + else + key.first.to_param + end + elsif key.is_a?(Hash) + key = key.to_a.sort{|a,b| a.first.to_s <=> b.first.to_s}.collect{|k,v| "#{k}=#{v}"}.to_param + else + key = key.to_param + end + end + + # Prefix a key with the namespace. The two values will be delimited with a colon. + def namespaced_key(key, options) + key = expanded_key(key) + namespace = options[:namespace] if options + prefix = namespace.is_a?(Proc) ? namespace.call : namespace + key = "#{prefix}:#{key}" if prefix + key end - def instrument(operation, key, options) + def instrument(operation, key, options = nil) log(operation, key, options) if self.class.instrument @@ -259,9 +508,118 @@ module ActiveSupport end end - def log(operation, key, options) - return unless logger && !silence? - logger.debug("Cache #{operation}: #{key}#{options ? " (#{options.inspect})" : ""}") + def log(operation, key, options = nil) + return unless logger && logger.debug? && !silence? + logger.debug("Cache #{operation}: #{key}#{options.blank? ? "" : " (#{options.inspect})"}") + end + end + + # Entry that is put into caches. It supports expiration time on entries and can compress values + # to save space in the cache. + class Entry + attr_reader :created_at, :expires_in + + DEFAULT_COMPRESS_LIMIT = 16.kilobytes + + class << self + # Create an entry with internal attributes set. This method is intended to be + # used by implementations that store cache entries in a native format instead + # of as serialized Ruby objects. + def create (raw_value, created_at, options = {}) + entry = new(nil) + entry.instance_variable_set(:@value, raw_value) + entry.instance_variable_set(:@created_at, created_at.to_f) + entry.instance_variable_set(:@compressed, !!options[:compressed]) + entry.instance_variable_set(:@expires_in, options[:expires_in]) + entry + end + end + + # Create a new cache entry for the specified value. Options supported are + # +:compress+, +:compress_threshold+, and +:expires_in+. + def initialize(value, options = {}) + @compressed = false + @expires_in = options[:expires_in] + @expires_in = @expires_in.to_f if @expires_in + @created_at = Time.now.to_f + if value + if should_compress?(value, options) + @value = Zlib::Deflate.deflate(Marshal.dump(value)) + @compressed = true + else + @value = value + end + else + @value = nil + end + end + + # Get the raw value. This value may be serialized and compressed. + def raw_value + @value + end + + # Get the value stored in the cache. + def value + if @value + val = compressed? ? Marshal.load(Zlib::Inflate.inflate(@value)) : @value + unless val.frozen? + val.freeze rescue nil + end + val + end + end + + def compressed? + @compressed + end + + # Check if the entry is expired. The +expires_in+ parameter can override the + # value set when the entry was created. + def expired? + if @expires_in && @created_at + @expires_in <= Time.now.to_f + true + else + false + end + end + + # Set a new time to live on the entry so it expires at the given time. + def expires_at=(time) + if time + @expires_in = time.to_f - @created_at + else + @expires_in = nil + end + end + + # Seconds since the epoch when the cache entry will expire. + def expires_at + @expires_in ? @created_at + @expires_in : nil + end + + # Get the size of the cached value. This could be less than value.size + # if the data is compressed. + def size + if @value.nil? + 0 + elsif @value.respond_to?(:bytesize) + @value.bytesize + else + Marshal.dump(@value).bytesize + end + end + + private + def should_compress?(value, options) + if options[:compress] && value + unless value.is_a?(Numeric) + compress_threshold = options[:compress_threshold] || DEFAULT_COMPRESS_LIMIT + serialized_value = value.is_a?(String) ? value : Marshal.dump(value) + return true if serialized_value.size >= compress_threshold + end + end + false end end end diff --git a/activesupport/lib/active_support/cache/compressed_mem_cache_store.rb b/activesupport/lib/active_support/cache/compressed_mem_cache_store.rb index d2370d78c5..7c7d1c4b00 100644 --- a/activesupport/lib/active_support/cache/compressed_mem_cache_store.rb +++ b/activesupport/lib/active_support/cache/compressed_mem_cache_store.rb @@ -1,21 +1,12 @@ -require 'active_support/gzip' - module ActiveSupport module Cache class CompressedMemCacheStore < MemCacheStore - def read(name, options = nil) - if value = super(name, (options || {}).merge(:raw => true)) - if raw?(options) - value - else - Marshal.load(ActiveSupport::Gzip.decompress(value)) - end - end - end - - def write(name, value, options = nil) - value = ActiveSupport::Gzip.compress(Marshal.dump(value)) unless raw?(options) - super(name, value, (options || {}).merge(:raw => true)) + def initialize(*args) + ActiveSupport::Deprecation.warn('ActiveSupport::Cache::CompressedMemCacheStore has been deprecated in favor of ActiveSupport::Cache::MemCacheStore(:compress => true).', caller) + addresses = args.dup + options = addresses.extract_options! + args = addresses + [options.merge(:compress => true)] + super(*args) end end end diff --git a/activesupport/lib/active_support/cache/file_store.rb b/activesupport/lib/active_support/cache/file_store.rb index 7521efe7c5..fc225e77a2 100644 --- a/activesupport/lib/active_support/cache/file_store.rb +++ b/activesupport/lib/active_support/cache/file_store.rb @@ -3,73 +3,171 @@ require 'active_support/core_ext/file/atomic' module ActiveSupport module Cache # A cache store implementation which stores everything on the filesystem. + # + # FileStore implements the Strategy::LocalCache strategy which implements + # an in memory cache inside of a block. class FileStore < Store attr_reader :cache_path - def initialize(cache_path) + DIR_FORMATTER = "%03X" + ESCAPE_FILENAME_CHARS = /[^a-z0-9_.-]/i + UNESCAPE_FILENAME_CHARS = /%[0-9A-F]{2}/ + + def initialize(cache_path, options = nil) + super(options) @cache_path = cache_path + extend Strategy::LocalCache end - # Reads a value from the cache. - # - # Possible options: - # - +:expires_in+ - the number of seconds that this value may stay in - # the cache. - def read(name, options = nil) - super do - file_name = real_file_path(name) - expires = expires_in(options) - - if File.exist?(file_name) && (expires <= 0 || Time.now - File.mtime(file_name) < expires) - File.open(file_name, 'rb') { |f| Marshal.load(f) } - end + def clear(options = nil) + root_dirs = Dir.entries(cache_path).reject{|f| ['.', '..'].include?(f)} + FileUtils.rm_r(root_dirs.collect{|f| File.join(cache_path, f)}) + end + + def cleanup(options = nil) + options = merged_options(options) + each_key(options) do |key| + entry = read_entry(key, options) + delete_entry(key, options) if entry && entry.expired? end end - # Writes a value to the cache. - def write(name, value, options = nil) - super do - ensure_cache_path(File.dirname(real_file_path(name))) - File.atomic_write(real_file_path(name), cache_path) { |f| Marshal.dump(value, f) } - value + def increment(name, amount = 1, options = nil) + file_name = key_file_path(namespaced_key(name, options)) + lock_file(file_name) do + options = merged_options(options) + if num = read(name, options) + num = num.to_i + amount + write(name, num, options) + num + else + nil + end end - rescue => e - logger.error "Couldn't create cache directory: #{name} (#{e.message})" if logger end - def delete(name, options = nil) - super do - File.delete(real_file_path(name)) + def decrement(name, amount = 1, options = nil) + file_name = key_file_path(namespaced_key(name, options)) + lock_file(file_name) do + options = merged_options(options) + if num = read(name, options) + num = num.to_i - amount + write(name, num, options) + num + else + nil + end end - rescue SystemCallError => e - # If there's no cache, then there's nothing to complain about end def delete_matched(matcher, options = nil) - super do - search_dir(@cache_path) do |f| - if f =~ matcher - begin - File.delete(f) - rescue SystemCallError => e - # If there's no cache, then there's nothing to complain about + options = merged_options(options) + instrument(:delete_matched, matcher.inspect) do + matcher = key_matcher(matcher, options) + search_dir(cache_path) do |path| + key = file_path_key(path) + delete_entry(key, options) if key.match(matcher) + end + end + end + + protected + + def read_entry(key, options) + file_name = key_file_path(key) + if File.exist?(file_name) + entry = File.open(file_name) { |f| Marshal.load(f) } + if entry && !entry.expired? && !entry.expires_in && !self.options[:expires_in] + # Check for deprecated use of +:expires_in+ option from versions < 3.0 + deprecated_expires_in = options[:expires_in] + if deprecated_expires_in + ActiveSupport::Deprecation.warn('Setting :expires_in on read has been deprecated in favor of setting it on write.', caller) + if entry.created_at + deprecated_expires_in.to_f <= Time.now.to_f + delete_entry(key, options) + entry = nil + end end end + entry end + rescue + nil end - end - def exist?(name, options = nil) - super do - File.exist?(real_file_path(name)) + def write_entry(key, entry, options) + file_name = key_file_path(key) + ensure_cache_path(File.dirname(file_name)) + File.atomic_write(file_name, 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) + begin + File.delete(file_name) + delete_empty_directories(File.dirname(file_name)) + true + rescue => e + # Just in case the error was caused by another process deleting the file first. + raise e if File.exist?(file_name) + false + end + end end - end private - def real_file_path(name) - '%s/%s.cache' % [@cache_path, name.gsub('?', '.').gsub(':', '.')] + # Lock a file for a block so only one process can modify it at a time. + def lock_file(file_name, &block) # :nodoc: + if File.exist?(file_name) + File.open(file_name, 'r') do |f| + begin + f.flock File::LOCK_EX + yield + ensure + f.flock File::LOCK_UN + end + end + else + yield + end + end + + # Translate a key into a file path. + def key_file_path(key) + fname = key.to_s.gsub(ESCAPE_FILENAME_CHARS){|match| "%#{match.ord.to_s(16).upcase}"} + hash = Zlib.adler32(fname) + hash, dir_1 = hash.divmod(0x1000) + dir_2 = hash.modulo(0x1000) + fname_paths = [] + # Make sure file name is < 255 characters so it doesn't exceed file system limits. + if fname.size <= 255 + fname_paths << fname + else + while fname.size <= 255 + fname_path << fname[0, 255] + fname = fname[255, -1] + end + end + File.join(cache_path, DIR_FORMATTER % dir_1, DIR_FORMATTER % dir_2, *fname_paths) + end + + # Translate a file path into a key. + def file_path_key(path) + fname = path[cache_path.size, path.size].split(File::SEPARATOR, 4).last + fname.gsub(UNESCAPE_FILENAME_CHARS){|match| $1.ord.to_s(16)} + end + + # Delete empty directories in the cache. + def delete_empty_directories(dir) + return if dir == cache_path + if Dir.entries(dir).reject{|f| ['.', '..'].include?(f)}.empty? + File.delete(dir) rescue nil + delete_empty_directories(File.dirname(dir)) + end end + # Make sure a file path's directories exist. def ensure_cache_path(path) FileUtils.makedirs(path) unless File.exist?(path) end diff --git a/activesupport/lib/active_support/cache/mem_cache_store.rb b/activesupport/lib/active_support/cache/mem_cache_store.rb index c56fedc12e..d8377a208f 100644 --- a/activesupport/lib/active_support/cache/mem_cache_store.rb +++ b/activesupport/lib/active_support/cache/mem_cache_store.rb @@ -1,5 +1,5 @@ require 'memcache' -require 'active_support/core_ext/array/extract_options' +require 'digest/md5' module ActiveSupport module Cache @@ -13,8 +13,9 @@ module ActiveSupport # and MemCacheStore will load balance between all available servers. If a # server goes down, then MemCacheStore will ignore it until it goes back # online. - # - Time-based expiry support. See #write and the <tt>:expires_in</tt> option. - # - Per-request in memory cache for all communication with the MemCache server(s). + # + # MemCacheStore implements the Strategy::LocalCache strategy which implements + # an in memory cache inside of a block. class MemCacheStore < Store module Response # :nodoc: STORED = "STORED\r\n" @@ -24,6 +25,8 @@ module ActiveSupport DELETED = "DELETED\r\n" end + ESCAPE_KEY_CHARS = /[\x00-\x20%\x7F-\xFF]/ + def self.build_mem_cache(*addresses) addresses = addresses.flatten options = addresses.extract_options! @@ -45,108 +48,139 @@ module ActiveSupport # require 'memcached' # gem install memcached; uses C bindings to libmemcached # ActiveSupport::Cache::MemCacheStore.new(Memcached::Rails.new("localhost:11211")) def initialize(*addresses) + addresses = addresses.flatten + options = addresses.extract_options! + super(options) + if addresses.first.respond_to?(:get) @data = addresses.first else - @data = self.class.build_mem_cache(*addresses) + mem_cache_options = options.dup + UNIVERSAL_OPTIONS.each{|name| mem_cache_options.delete(name)} + @data = self.class.build_mem_cache(*(addresses + [mem_cache_options])) end extend Strategy::LocalCache + extend LocalCacheWithRaw end - # Reads multiple keys from the cache. - def read_multi(*keys) - @data.get_multi keys - end - - def read(key, options = nil) # :nodoc: - super do - @data.get(key, raw?(options)) - end - rescue MemCache::MemCacheError => e - logger.error("MemCacheError (#{e}): #{e.message}") if logger - nil - end - - # Writes a value to the cache. - # - # Possible options: - # - <tt>:unless_exist</tt> - set to true if you don't want to update the cache - # if the key is already set. - # - <tt>:expires_in</tt> - the number of seconds that this value may stay in - # the cache. See ActiveSupport::Cache::Store#write for an example. - def write(key, value, options = nil) - super do - method = options && options[:unless_exist] ? :add : :set - # memcache-client will break the connection if you send it an integer - # in raw mode, so we convert it to a string to be sure it continues working. - value = value.to_s if raw?(options) - response = @data.send(method, key, value, expires_in(options), raw?(options)) - response == Response::STORED - end - rescue MemCache::MemCacheError => e - logger.error("MemCacheError (#{e}): #{e.message}") if logger - false - end - - def delete(key, options = nil) # :nodoc: - super do - response = @data.delete(key) - response == Response::DELETED - end - rescue MemCache::MemCacheError => e - logger.error("MemCacheError (#{e}): #{e.message}") if logger - false - end - - def exist?(key, options = nil) # :nodoc: - # Doesn't call super, cause exist? in memcache is in fact a read - # But who cares? Reading is very fast anyway - # Local cache is checked first, if it doesn't know then memcache itself is read from - super do - !read(key, options).nil? + # Reads multiple keys from the cache using a single call to the + # servers for all keys. Options can be passed in the last argument. + def read_multi(*names) + options = names.extract_options! + options = merged_options(options) + keys_to_names = names.inject({}){|map, name| map[escape_key(namespaced_key(name, options))] = name; map} + raw_values = @data.get_multi(keys_to_names.keys, :raw => true) + values = {} + raw_values.each do |key, value| + entry = deserialize_entry(value) + values[keys_to_names[key]] = entry.value unless entry.expired? end + values end - def increment(key, amount = 1) # :nodoc: - response = instrument(:increment, key, :amount => amount) do - @data.incr(key, amount) + # Increment a cached value. This method uses the memcached incr atomic + # operator and can only be used on values written with the :raw option. + # Calling it on a value not stored with :raw will initialize that value + # to zero. + def increment(name, amount = 1, options = nil) # :nodoc: + options = merged_options(options) + response = instrument(:increment, name, :amount => amount) do + @data.incr(escape_key(namespaced_key(name, options)), amount) end - - response == Response::NOT_FOUND ? nil : response + response == Response::NOT_FOUND ? nil : response.to_i rescue MemCache::MemCacheError nil end - def decrement(key, amount = 1) # :nodoc: - response = instrument(:decrement, key, :amount => amount) do - @data.decr(key, amount) + # Decrement a cached value. This method uses the memcached decr atomic + # operator and can only be used on values written with the :raw option. + # Calling it on a value not stored with :raw will initialize that value + # to zero. + def decrement(name, amount = 1, options = nil) # :nodoc: + options = merged_options(options) + response = instrument(:decrement, name, :amount => amount) do + @data.decr(escape_key(namespaced_key(name, options)), amount) end - - response == Response::NOT_FOUND ? nil : response + response == Response::NOT_FOUND ? nil : response.to_i rescue MemCache::MemCacheError nil end - def delete_matched(matcher, options = nil) # :nodoc: - # don't do any local caching at present, just pass - # through and let the error happen - super - raise "Not supported by Memcache" - end - - def clear + # Clear the entire cache on all memcached servers. This method should + # be used with care when using a shared cache. + def clear(options = nil) @data.flush_all end + # Get the statistics from the memcached servers. def stats @data.stats end + protected + # Read an entry from the cache. + def read_entry(key, options) # :nodoc: + deserialize_entry(@data.get(escape_key(key), true)) + rescue MemCache::MemCacheError => e + logger.error("MemCacheError (#{e}): #{e.message}") if logger + nil + end + + # Write an entry to the cache. + def write_entry(key, entry, options) # :nodoc: + method = options && options[:unless_exist] ? :add : :set + value = options[:raw] ? entry.value.to_s : entry + expires_in = options[:expires_in].to_i + if expires_in > 0 && !options[:raw] + # Set the memcache expire a few minutes in the future to support race condition ttls on read + expires_in += 5.minutes + end + response = @data.send(method, escape_key(key), value, expires_in, options[:raw]) + response == Response::STORED + rescue MemCache::MemCacheError => e + logger.error("MemCacheError (#{e}): #{e.message}") if logger + false + end + + # Delete an entry from the cache. + def delete_entry(key, options) # :nodoc: + response = @data.delete(escape_key(key)) + response == Response::DELETED + rescue MemCache::MemCacheError => e + logger.error("MemCacheError (#{e}): #{e.message}") if logger + false + end + private - def raw?(options) - options && options[:raw] + def escape_key(key) + key = key.to_s.gsub(ESCAPE_KEY_CHARS){|match| "%#{match[0].to_s(16).upcase}"} + key = "#{key[0, 213]}:md5:#{Digest::MD5.hexdigest(key)}" if key.size > 250 + 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 + + # Provide support for raw values in the local cache strategy. + module LocalCacheWithRaw # :nodoc: + protected + def write_entry(key, entry, options) # :nodoc: + retval = super + if options[:raw] && local_cache && retval + raw_entry = Entry.new(entry.value.to_s) + raw_entry.expires_at = entry.expires_at + local_cache.write_entry(key, raw_entry, options) + end + retval + end + 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 379922f986..b1d14a0d8f 100644 --- a/activesupport/lib/active_support/cache/memory_store.rb +++ b/activesupport/lib/active_support/cache/memory_store.rb @@ -1,4 +1,4 @@ -require 'active_support/core_ext/object/duplicable' +require 'monitor' module ActiveSupport module Cache @@ -6,60 +6,154 @@ module ActiveSupport # same process. If you're running multiple Ruby on Rails server processes # (which is the case if you're using mongrel_cluster or Phusion Passenger), # then this means that your Rails server process instances won't be able - # to share cache data with each other. If your application never performs - # manual cache item expiry (e.g. when you're using generational cache keys), - # then using MemoryStore is ok. Otherwise, consider carefully whether you - # should be using this cache store. + # to share cache data with each other and this may not be the most + # appropriate cache for you. # - # MemoryStore is not only able to store strings, but also arbitrary Ruby - # objects. + # This cache has a bounded size specified by the :size options to the + # initializer (default is 32Mb). When the cache exceeds the alotted size, + # a cleanup will occur which tries to prune the cache down to three quarters + # of the maximum size by removing the least recently used entries. # - # MemoryStore is not thread-safe. Use SynchronizedMemoryStore instead - # if you need thread-safety. + # MemoryStore is thread-safe. class MemoryStore < Store - def initialize + def initialize(options = nil) + options ||= {} + super(options) @data = {} + @key_access = {} + @max_size = options[:size] || 32.megabytes + @max_prune_time = options[:max_prune_time] || 2 + @cache_size = 0 + @monitor = Monitor.new + @pruning = false end - def read_multi(*names) - results = {} - names.each { |n| results[n] = read(n) } - results + def clear(options = nil) + synchronize do + @data.clear + @key_access.clear + @cache_size = 0 + end end - def read(name, options = nil) - super do - @data[name] + def cleanup(options = nil) + options = merged_options(options) + instrument(:cleanup, :size => @data.size) do + keys = synchronize{ @data.keys } + keys.each do |key| + entry = @data[key] + delete_entry(key, options) if entry && entry.expired? + end end end - def write(name, value, options = nil) - super do - @data[name] = (value.duplicable? ? value.dup : value).freeze + # Prune the cache down so the entries fit within the specified memory size by removing + # the least recently accessed entries. + def prune(target_size, max_time = nil) + return if pruning? + @pruning = true + begin + start_time = Time.now + cleanup + instrument(:prune, target_size, :from => @cache_size) do + keys = synchronize{ @key_access.keys.sort{|a,b| @key_access[a].to_f <=> @key_access[b].to_f} } + keys.each do |key| + delete_entry(key, options) + return if @cache_size <= target_size || (max_time && Time.now - start_time > max_time) + end + end + ensure + @pruning = false end end - def delete(name, options = nil) - super do - @data.delete(name) + # Return true if the cache is currently be pruned to remove older entries. + def pruning? + @pruning + end + + # 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 end - def delete_matched(matcher, options = nil) - super do - @data.delete_if { |k,v| k =~ matcher } + # 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 end - def exist?(name, options = nil) - super do - @data.has_key?(name) + def delete_matched(matcher, options = nil) + options = merged_options(options) + instrument(:delete_matched, matcher.inspect) do + matcher = key_matcher(matcher, options) + keys = synchronize { @data.keys } + keys.each do |key| + delete_entry(key, options) if key.match(matcher) + end end end - def clear - @data.clear + def inspect # :nodoc: + "<##{self.class.name} entries=#{@data.size}, size=#{@cache_size}, options=#{@options.inspect}>" + end + + # Synchronize calls to the cache. This should be called wherever the underlying cache implementation + # is not thread safe. + def synchronize(&block) # :nodoc: + @monitor.synchronize(&block) end + + protected + def read_entry(key, options) # :nodoc: + entry = @data[key] + synchronize do + if entry + @key_access[key] = Time.now.to_f + else + @key_access.delete(key) + end + end + entry + end + + def write_entry(key, entry, options) # :nodoc: + synchronize do + old_entry = @data[key] + @cache_size -= old_entry.size if old_entry + @cache_size += entry.size + @key_access[key] = Time.now.to_f + @data[key] = entry + prune(@max_size * 0.75, @max_prune_time) if @cache_size > @max_size + true + end + end + + def delete_entry(key, options) # :nodoc: + synchronize do + @key_access.delete(key) + entry = @data.delete(key) + @cache_size -= entry.size if entry + !!entry + 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 bbbd643736..8942587ac8 100644 --- a/activesupport/lib/active_support/cache/strategy/local_cache.rb +++ b/activesupport/lib/active_support/cache/strategy/local_cache.rb @@ -4,17 +4,54 @@ require 'active_support/core_ext/string/inflections' module ActiveSupport module Cache module Strategy + # Caches that implement LocalCache will be backed by an in memory cache for the + # duration of a block. Repeated calls to the cache for the same key will hit the + # in memory cache for faster access. module LocalCache - # this allows caching of the fact that there is nothing in the remote cache - NULL = 'remote_cache_store:null' + # Simple memory backed cache. This cache is not thread safe but is intended only + # for serving as a temporary memory cache for a single thread. + class LocalStore < Store + def initialize + super + @data = {} + end + + # Since it isn't thread safe, don't allow synchronizing. + def synchronize # :nodoc: + yield + end + + def clear(options = nil) + @data.clear + end + + def read_entry(key, options) + @data[key] + end + def write_entry(key, value, options) + @data[key] = value + true + end + + def delete_entry(key, options) + !!@data.delete(key) + end + end + + # Use a local cache to front for the cache for the duration of a block. def with_local_cache - Thread.current[thread_local_key] = MemoryStore.new - yield - ensure - Thread.current[thread_local_key] = nil + save_val = Thread.current[thread_local_key] + begin + Thread.current[thread_local_key] = LocalStore.new + yield + ensure + Thread.current[thread_local_key] = save_val + end end + # Middleware class can be inserted as a Rack handler to use a local cache for the + # duration of a request. def middleware @middleware ||= begin klass = Class.new @@ -24,7 +61,7 @@ module ActiveSupport end def call(env) - Thread.current[:#{thread_local_key}] = MemoryStore.new + Thread.current[:#{thread_local_key}] = LocalStore.new @app.call(env) ensure Thread.current[:#{thread_local_key}] = nil @@ -39,73 +76,86 @@ module ActiveSupport end end - def read(key, options = nil) - value = local_cache && local_cache.read(key) - if value == NULL - nil - elsif value.nil? - value = super - local_cache.mute { local_cache.write(key, value || NULL) } if local_cache - value.duplicable? ? value.dup : value - else - # forcing the value to be immutable - value.duplicable? ? value.dup : value - end - end - - def write(key, value, options = nil) - value = value.to_s if respond_to?(:raw?) && raw?(options) - local_cache.mute { local_cache.write(key, value || NULL) } if local_cache + def clear(options = nil) # :nodoc: + local_cache.clear(options) if local_cache super end - def delete(key, options = nil) - local_cache.mute { local_cache.write(key, NULL) } if local_cache + def cleanup(options = nil) # :nodoc: + local_cache.clear(options) if local_cache super end - def exist(key, options = nil) - value = local_cache.read(key) if local_cache - if value == NULL - false - elsif value - true - else - super + def increment(name, amount = 1, options = nil) # :nodoc: + value = bypass_local_cache{super} + if local_cache + local_cache.mute do + if value + local_cache.write(name, value, options) + else + local_cache.delete(name, options) + end + end end + value end - def increment(key, amount = 1) - if value = super - local_cache.mute { local_cache.write(key, value.to_s) } if local_cache - value - else - nil + def decrement(name, amount = 1, options = nil) # :nodoc: + value = bypass_local_cache{super} + if local_cache + local_cache.mute do + if value + local_cache.write(name, value, options) + else + local_cache.delete(name, options) + end + end end + value end - def decrement(key, amount = 1) - if value = super - local_cache.mute { local_cache.write(key, value.to_s) } if local_cache - value - else - nil + protected + def read_entry(key, options) # :nodoc: + if local_cache + entry = local_cache.read_entry(key, options) + unless entry + entry = super + local_cache.write_entry(key, entry, options) + end + entry + else + super + end end - end - def clear - local_cache.clear if local_cache - super - end + def write_entry(key, entry, options) # :nodoc: + local_cache.write_entry(key, entry, options) if local_cache + super + end + + def delete_entry(key, options) # :nodoc: + local_cache.delete_entry(key, options) if local_cache + super + end private def thread_local_key - @thread_local_key ||= "#{self.class.name.underscore}_local_cache".gsub("/", "_").to_sym + @thread_local_key ||= "#{self.class.name.underscore}_local_cache_#{self.object_id}".gsub("/", "_").to_sym end def local_cache Thread.current[thread_local_key] end + + def bypass_local_cache + save_cache = Thread.current[thread_local_key] + begin + Thread.current[thread_local_key] = nil + yield + ensure + Thread.current[thread_local_key] = save_cache + end + end end end end diff --git a/activesupport/lib/active_support/cache/synchronized_memory_store.rb b/activesupport/lib/active_support/cache/synchronized_memory_store.rb index ea03a119c6..37caa6b6f1 100644 --- a/activesupport/lib/active_support/cache/synchronized_memory_store.rb +++ b/activesupport/lib/active_support/cache/synchronized_memory_store.rb @@ -2,45 +2,9 @@ module ActiveSupport module Cache # Like MemoryStore, but thread-safe. class SynchronizedMemoryStore < MemoryStore - def initialize + def initialize(*args) + ActiveSupport::Deprecation.warn('ActiveSupport::Cache::SynchronizedMemoryStore has been deprecated in favor of ActiveSupport::Cache::MemoryStore.', caller) super - @guard = Monitor.new - end - - def fetch(key, options = {}) - @guard.synchronize { super } - end - - def read(name, options = nil) - @guard.synchronize { super } - end - - def write(name, value, options = nil) - @guard.synchronize { super } - end - - def delete(name, options = nil) - @guard.synchronize { super } - end - - def delete_matched(matcher, options = nil) - @guard.synchronize { super } - end - - def exist?(name,options = nil) - @guard.synchronize { super } - end - - def increment(key, amount = 1) - @guard.synchronize { super } - end - - def decrement(key, amount = 1) - @guard.synchronize { super } - end - - def clear - @guard.synchronize { super } end end end diff --git a/activesupport/lib/active_support/configurable.rb b/activesupport/lib/active_support/configurable.rb index 890f465ce1..f562e17c75 100644 --- a/activesupport/lib/active_support/configurable.rb +++ b/activesupport/lib/active_support/configurable.rb @@ -1,35 +1,36 @@ -require "active_support/concern" +require 'active_support/concern' +require 'active_support/ordered_options' +require 'active_support/core_ext/kernel/singleton_class' +require 'active_support/core_ext/module/delegation' module ActiveSupport module Configurable extend ActiveSupport::Concern module ClassMethods - def get_config - module_parts = name.split("::") - modules = [Object] - module_parts.each {|name| modules.push modules.last.const_get(name) } - modules.reverse_each do |mod| - return mod.const_get(:DEFAULT_CONFIG) if const_defined?(:DEFAULT_CONFIG) - end - {} - end - def config - self.config = get_config unless @config - @config + @config ||= ActiveSupport::InheritableOptions.new(superclass.respond_to?(:config) ? superclass.config : {}) end - def config=(hash) - @config = ActiveSupport::OrderedOptions.new - hash.each do |key, value| - @config[key] = value + def configure + yield config + end + + def config_accessor(*names) + names.each do |name| + code, line = <<-RUBY, __LINE__ + 1 + def #{name}; config.#{name}; end + def #{name}=(value); config.#{name} = value; end + RUBY + + singleton_class.class_eval code, __FILE__, line + class_eval code, __FILE__, line end end end def config - self.class.config + @config ||= ActiveSupport::InheritableOptions.new(self.class.config) end end end
\ No newline at end of file diff --git a/activesupport/lib/active_support/core_ext/kernel/reporting.rb b/activesupport/lib/active_support/core_ext/kernel/reporting.rb index ac35db6ab6..7c455f66d5 100644 --- a/activesupport/lib/active_support/core_ext/kernel/reporting.rb +++ b/activesupport/lib/active_support/core_ext/kernel/reporting.rb @@ -1,3 +1,4 @@ +require 'rbconfig' module Kernel # Sets $VERBOSE to nil for the duration of the block and back to its original value afterwards. # @@ -37,7 +38,7 @@ module Kernel # puts 'But this will' def silence_stream(stream) old_stream = stream.dup - stream.reopen(RUBY_PLATFORM =~ /mswin|mingw/ ? 'NUL:' : '/dev/null') + stream.reopen(Config::CONFIG['host_os'] =~ /mswin|mingw/ ? 'NUL:' : '/dev/null') stream.sync = true yield ensure diff --git a/activesupport/lib/active_support/core_ext/string/conversions.rb b/activesupport/lib/active_support/core_ext/string/conversions.rb index 4cc36147f8..6a243fe982 100644 --- a/activesupport/lib/active_support/core_ext/string/conversions.rb +++ b/activesupport/lib/active_support/core_ext/string/conversions.rb @@ -30,16 +30,19 @@ class String # Form can be either :utc (default) or :local. def to_time(form = :utc) + return nil if self.blank? d = ::Date._parse(self, false).values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction).map { |arg| arg || 0 } d[6] *= 1000000 ::Time.send("#{form}_time", *d) end def to_date + return nil if self.blank? ::Date.new(*::Date._parse(self, false).values_at(:year, :mon, :mday)) end def to_datetime + return nil if self.blank? d = ::Date._parse(self, false).values_at(:year, :mon, :mday, :hour, :min, :sec, :zone, :sec_fraction).map { |arg| arg || 0 } d[5] += d.pop ::DateTime.civil(*d) diff --git a/activesupport/lib/active_support/core_ext/uri.rb b/activesupport/lib/active_support/core_ext/uri.rb index ca1be8b7e9..28eabd2111 100644 --- a/activesupport/lib/active_support/core_ext/uri.rb +++ b/activesupport/lib/active_support/core_ext/uri.rb @@ -1,8 +1,9 @@ +# encoding: utf-8 + if RUBY_VERSION >= '1.9' require 'uri' str = "\xE6\x97\xA5\xE6\x9C\xAC\xE8\xAA\x9E" # Ni-ho-nn-go in UTF-8, means Japanese. - str.force_encoding(Encoding::UTF_8) if str.respond_to?(:force_encoding) parser = URI::Parser.new unless str == parser.unescape(parser.escape(str)) diff --git a/activesupport/lib/active_support/hash_with_indifferent_access.rb b/activesupport/lib/active_support/hash_with_indifferent_access.rb index 8241b69c8b..f64f0f44cc 100644 --- a/activesupport/lib/active_support/hash_with_indifferent_access.rb +++ b/activesupport/lib/active_support/hash_with_indifferent_access.rb @@ -1,3 +1,5 @@ +require 'active_support/core_ext/hash/keys' + # This class has dubious semantics and we only have it so that # people can write params[:key] instead of params['key'] # and they get the same value for both keys. @@ -112,7 +114,9 @@ module ActiveSupport end def stringify_keys!; self end - def symbolize_keys!; self end + def stringify_keys; dup end + undef :symbolize_keys! + def symbolize_keys; to_hash.symbolize_keys end def to_options!; self end # Convert to a Hash with String keys. diff --git a/activesupport/lib/active_support/json/encoding.rb b/activesupport/lib/active_support/json/encoding.rb index 8ba45f7ea2..e692f6d142 100644 --- a/activesupport/lib/active_support/json/encoding.rb +++ b/activesupport/lib/active_support/json/encoding.rb @@ -1,4 +1,5 @@ # encoding: utf-8 +require 'bigdecimal' require 'active_support/core_ext/array/wrap' require 'active_support/core_ext/hash/except' require 'active_support/core_ext/hash/slice' @@ -102,7 +103,9 @@ module ActiveSupport end def escape(string) - string = string.dup.force_encoding(::Encoding::BINARY) if string.respond_to?(:force_encoding) + if string.respond_to?(:force_encoding) + string = string.encode(::Encoding::UTF_8, :undef => :replace).force_encoding(::Encoding::BINARY) + end json = string. gsub(escape_regex) { |s| ESCAPED_CHARS[s] }. gsub(/([\xC0-\xDF][\x80-\xBF]| @@ -110,7 +113,9 @@ module ActiveSupport [\xF0-\xF7][\x80-\xBF]{3})+/nx) { |s| s.unpack("U*").pack("n*").unpack("H*")[0].gsub(/.{4}/n, '\\\\u\&') } - %("#{json}") + json = %("#{json}") + json.force_encoding(::Encoding::UTF_8) if json.respond_to?(:force_encoding) + json end end @@ -128,7 +133,13 @@ class Object ActiveSupport::JSON.encode(self, options) end - def as_json(options = nil) instance_values end #:nodoc: + def as_json(options = nil) #:nodoc: + if respond_to?(:to_hash) + to_hash + else + instance_values + end + end end # A string that returns itself as its JSON-encoded form. @@ -166,9 +177,12 @@ class Numeric def encode_json(encoder) to_s end #:nodoc: end +class BigDecimal + def as_json(options = nil) to_s end #:nodoc: +end + class Regexp - def as_json(options = nil) self end #:nodoc: - def encode_json(encoder) inspect end #:nodoc: + def as_json(options = nil) to_s end #:nodoc: end module Enumerable diff --git a/activesupport/lib/active_support/multibyte/chars.rb b/activesupport/lib/active_support/multibyte/chars.rb index 38007fd4e7..4ade1158fd 100644 --- a/activesupport/lib/active_support/multibyte/chars.rb +++ b/activesupport/lib/active_support/multibyte/chars.rb @@ -72,8 +72,8 @@ module ActiveSupport #:nodoc: def self.codepoints_to_pattern(array_of_codepoints) #:nodoc: array_of_codepoints.collect{ |e| [e].pack 'U*' }.join('|') end - UNICODE_TRAILERS_PAT = /(#{codepoints_to_pattern(UNICODE_LEADERS_AND_TRAILERS)})+\Z/ - UNICODE_LEADERS_PAT = /\A(#{codepoints_to_pattern(UNICODE_LEADERS_AND_TRAILERS)})+/ + UNICODE_TRAILERS_PAT = /(#{codepoints_to_pattern(UNICODE_LEADERS_AND_TRAILERS)})+\Z/u + UNICODE_LEADERS_PAT = /\A(#{codepoints_to_pattern(UNICODE_LEADERS_AND_TRAILERS)})+/u UTF8_PAT = ActiveSupport::Multibyte::VALID_CHARACTER['UTF-8'] diff --git a/activesupport/lib/active_support/notifications/fanout.rb b/activesupport/lib/active_support/notifications/fanout.rb index a3ddc7705a..300ec842a9 100644 --- a/activesupport/lib/active_support/notifications/fanout.rb +++ b/activesupport/lib/active_support/notifications/fanout.rb @@ -19,8 +19,8 @@ module ActiveSupport end def unsubscribe(subscriber) - @subscribers.delete(subscriber) @listeners_for.clear + @subscribers.reject! {|s| s.matches?(subscriber)} end def publish(name, *args) @@ -60,7 +60,7 @@ module ActiveSupport end def publish(*args) - return unless matches?(args.first) + return unless subscribed_to?(args.first) push(*args) true end @@ -69,10 +69,20 @@ module ActiveSupport true end - private - def matches?(name) - !@pattern || @pattern =~ name.to_s + def subscribed_to?(name) + !@pattern || @pattern =~ name.to_s + end + + def matches?(subscriber_or_name) + case subscriber_or_name + when String + @pattern && @pattern =~ subscriber_or_name + when self + true end + end + + private def push(*args) @block.call(*args) diff --git a/activesupport/lib/active_support/testing/isolation.rb b/activesupport/lib/active_support/testing/isolation.rb index 9507dbf473..69df399cde 100644 --- a/activesupport/lib/active_support/testing/isolation.rb +++ b/activesupport/lib/active_support/testing/isolation.rb @@ -1,3 +1,4 @@ +require 'rbconfig' module ActiveSupport module Testing class RemoteError < StandardError @@ -33,7 +34,7 @@ module ActiveSupport module Isolation def self.forking_env? - !ENV["NO_FORK"] && RUBY_PLATFORM !~ /mswin|mingw|java/ + !ENV["NO_FORK"] && ((Config::CONFIG['host_os'] !~ /mswin|mingw/) && (RUBY_PLATFORM !~ /java/)) end def self.included(base) diff --git a/activesupport/lib/active_support/testing/setup_and_teardown.rb b/activesupport/lib/active_support/testing/setup_and_teardown.rb index 6ce9495cee..d8942c3974 100644 --- a/activesupport/lib/active_support/testing/setup_and_teardown.rb +++ b/activesupport/lib/active_support/testing/setup_and_teardown.rb @@ -1,3 +1,6 @@ +require 'active_support/concern' +require 'active_support/callbacks' + module ActiveSupport module Testing module SetupAndTeardown diff --git a/activesupport/lib/active_support/values/time_zone.rb b/activesupport/lib/active_support/values/time_zone.rb index 9db6bbafca..2ac5134911 100644 --- a/activesupport/lib/active_support/values/time_zone.rb +++ b/activesupport/lib/active_support/values/time_zone.rb @@ -190,6 +190,7 @@ module ActiveSupport include Comparable attr_reader :name + attr_reader :tzinfo # Create a new TimeZone object with the given name and offset. The # offset is the number of seconds that this time zone is offset from UTC @@ -198,7 +199,7 @@ module ActiveSupport def initialize(name, utc_offset = nil, tzinfo = nil) @name = name @utc_offset = utc_offset - @tzinfo = tzinfo + @tzinfo = tzinfo || TimeZone.find_tzinfo(name) @current_period = nil end @@ -310,32 +311,10 @@ module ActiveSupport tzinfo.period_for_local(time, dst) end - def tzinfo - @tzinfo ||= TimeZone.find_tzinfo(name) - end - # TODO: Preload instead of lazy load for thread safety def self.find_tzinfo(name) require 'tzinfo' unless defined?(::TZInfo) - ::TZInfo::Timezone.get(MAPPING[name] || name) - rescue TZInfo::InvalidTimezoneIdentifier - nil - end - - unless const_defined?(:ZONES) - ZONES = [] - ZONES_MAP = {} - MAPPING.each_key do |place| - place.freeze - zone = new(place) - ZONES << zone - ZONES_MAP[place] = zone - end - ZONES.sort! - ZONES.freeze - - US_ZONES = ZONES.find_all { |z| z.name =~ /US|Arizona|Indiana|Hawaii|Alaska/ } - US_ZONES.freeze + ::TZInfo::TimezoneProxy.new(MAPPING[name] || name) end class << self @@ -352,7 +331,11 @@ module ActiveSupport # TimeZone objects per time zone, in many cases, to make it easier # for users to find their own time zone. def all - ZONES + @zones ||= zones_map.values.sort + end + + def zones_map + @zones_map ||= Hash[MAPPING.map { |place, _| [place, create(place)] }] end # Locate a specific time zone object. If the argument is a string, it @@ -363,7 +346,7 @@ module ActiveSupport def [](arg) case arg when String - ZONES_MAP[arg] ||= lookup(arg) + zones_map[arg] ||= lookup(arg) when Numeric, ActiveSupport::Duration arg *= 3600 if arg.abs <= 13 all.find { |z| z.utc_offset == arg.to_i } @@ -375,7 +358,7 @@ module ActiveSupport # A convenience method for returning a collection of TimeZone objects # for time zones in the USA. def us_zones - US_ZONES + @us_zones ||= all.find_all { |z| z.name =~ /US|Arizona|Indiana|Hawaii|Alaska/ } end private diff --git a/activesupport/test/caching_test.rb b/activesupport/test/caching_test.rb index e62e7ef9aa..d9ff1207e7 100644 --- a/activesupport/test/caching_test.rb +++ b/activesupport/test/caching_test.rb @@ -4,6 +4,7 @@ require 'active_support/cache' class CacheKeyTest < ActiveSupport::TestCase def test_expand_cache_key + assert_equal '1/2/true', ActiveSupport::Cache.expand_cache_key([1, '2', true]) assert_equal 'name/1/2/true', ActiveSupport::Cache.expand_cache_key([1, '2', true], :name) end end @@ -43,9 +44,10 @@ class CacheStoreSettingTest < ActiveSupport::TestCase end def test_mem_cache_fragment_cache_store_with_options - MemCache.expects(:new).with(%w[localhost 192.168.1.1], { :namespace => "foo" }) - store = ActiveSupport::Cache.lookup_store :mem_cache_store, "localhost", '192.168.1.1', :namespace => 'foo' + MemCache.expects(:new).with(%w[localhost 192.168.1.1], { :timeout => 10 }) + store = ActiveSupport::Cache.lookup_store :mem_cache_store, "localhost", '192.168.1.1', :namespace => 'foo', :timeout => 10 assert_kind_of(ActiveSupport::Cache::MemCacheStore, store) + assert_equal 'foo', store.options[:namespace] end def test_object_assigned_fragment_cache_store @@ -55,124 +57,170 @@ class CacheStoreSettingTest < ActiveSupport::TestCase end end -class CacheStoreTest < ActiveSupport::TestCase - def setup - @cache = ActiveSupport::Cache.lookup_store(:memory_store) +class CacheStoreNamespaceTest < ActiveSupport::TestCase + def test_static_namespace + cache = ActiveSupport::Cache.lookup_store(:memory_store, :namespace => "tester") + cache.write("foo", "bar") + assert_equal "bar", cache.read("foo") + assert_equal "bar", cache.instance_variable_get(:@data)["tester:foo"].value + end + + def test_proc_namespace + test_val = "tester" + proc = lambda{test_val} + cache = ActiveSupport::Cache.lookup_store(:memory_store, :namespace => proc) + cache.write("foo", "bar") + assert_equal "bar", cache.read("foo") + assert_equal "bar", cache.instance_variable_get(:@data)["tester:foo"].value + end + + def test_delete_matched_key_start + cache = ActiveSupport::Cache.lookup_store(:memory_store, :namespace => "tester") + cache.write("foo", "bar") + cache.write("fu", "baz") + cache.delete_matched(/^fo/) + assert_equal false, cache.exist?("foo") + assert_equal true, cache.exist?("fu") + end + + def test_delete_matched_key + cache = ActiveSupport::Cache.lookup_store(:memory_store, :namespace => "foo") + cache.write("foo", "bar") + cache.write("fu", "baz") + cache.delete_matched(/OO/i) + assert_equal false, cache.exist?("foo") + assert_equal true, cache.exist?("fu") + end +end + +# Tests the base functionality that should be identical across all cache stores. +module CacheStoreBehavior + def test_should_read_and_write_strings + assert_equal true, @cache.write('foo', 'bar') + assert_equal 'bar', @cache.read('foo') + end + + def test_should_overwrite + @cache.write('foo', 'bar') + @cache.write('foo', 'baz') + assert_equal 'baz', @cache.read('foo') end def test_fetch_without_cache_miss - @cache.stubs(:read).with('foo', {}).returns('bar') + @cache.write('foo', 'bar') @cache.expects(:write).never assert_equal 'bar', @cache.fetch('foo') { 'baz' } end def test_fetch_with_cache_miss - @cache.stubs(:read).with('foo', {}).returns(nil) - @cache.expects(:write).with('foo', 'baz', {}) + @cache.expects(:write).with('foo', 'baz', @cache.options) assert_equal 'baz', @cache.fetch('foo') { 'baz' } end def test_fetch_with_forced_cache_miss + @cache.write('foo', 'bar') @cache.expects(:read).never - @cache.expects(:write).with('foo', 'bar', :force => true) + @cache.expects(:write).with('foo', 'bar', @cache.options.merge(:force => true)) @cache.fetch('foo', :force => true) { 'bar' } end -end -# Tests the base functionality that should be identical across all cache stores. -module CacheStoreBehavior - def test_should_read_and_write_strings - @cache.write('foo', 'bar') - assert_equal 'bar', @cache.read('foo') + def test_fetch_with_cached_nil + @cache.write('foo', nil) + @cache.expects(:write).never + assert_nil @cache.fetch('foo') { 'baz' } end def test_should_read_and_write_hash - @cache.write('foo', {:a => "b"}) + assert_equal true, @cache.write('foo', {:a => "b"}) assert_equal({:a => "b"}, @cache.read('foo')) end def test_should_read_and_write_integer - @cache.write('foo', 1) + assert_equal true, @cache.write('foo', 1) assert_equal 1, @cache.read('foo') end def test_should_read_and_write_nil - @cache.write('foo', nil) + assert_equal true, @cache.write('foo', nil) assert_equal nil, @cache.read('foo') end - def test_fetch_without_cache_miss + def test_read_multi @cache.write('foo', 'bar') - assert_equal 'bar', @cache.fetch('foo') { 'baz' } + @cache.write('fu', 'baz') + @cache.write('fud', 'biz') + assert_equal({"foo" => "bar", "fu" => "baz"}, @cache.read_multi('foo', 'fu')) end - def test_fetch_with_cache_miss - assert_equal 'baz', @cache.fetch('foo') { 'baz' } + def test_read_and_write_compressed_small_data + @cache.write('foo', 'bar', :compress => true) + raw_value = @cache.send(:read_entry, 'foo', {}).raw_value + assert_equal 'bar', @cache.read('foo') + assert_equal 'bar', raw_value end - def test_fetch_with_forced_cache_miss - @cache.fetch('foo', :force => true) { 'bar' } + def test_read_and_write_compressed_large_data + @cache.write('foo', 'bar', :compress => true, :compress_threshold => 2) + raw_value = @cache.send(:read_entry, 'foo', {}).raw_value + assert_equal 'bar', @cache.read('foo') + assert_equal 'bar', Marshal.load(Zlib::Inflate.inflate(raw_value)) end - def test_increment - @cache.write('foo', 1, :raw => true) - assert_equal 1, @cache.read('foo', :raw => true).to_i - assert_equal 2, @cache.increment('foo') - assert_equal 2, @cache.read('foo', :raw => true).to_i - assert_equal 3, @cache.increment('foo') - assert_equal 3, @cache.read('foo', :raw => true).to_i + def test_read_and_write_compressed_nil + @cache.write('foo', nil, :compress => true) + assert_nil @cache.read('foo') end - def test_decrement - @cache.write('foo', 3, :raw => true) - assert_equal 3, @cache.read('foo', :raw => true).to_i - assert_equal 2, @cache.decrement('foo') - assert_equal 2, @cache.read('foo', :raw => true).to_i - assert_equal 1, @cache.decrement('foo') - assert_equal 1, @cache.read('foo', :raw => true).to_i + def test_cache_key + obj = Object.new + def obj.cache_key + :foo + end + @cache.write(obj, "bar") + assert_equal "bar", @cache.read("foo") end - def test_exist - @cache.write('foo', 'bar') - assert @cache.exist?('foo') - assert !@cache.exist?('bar') + def test_param_as_cache_key + obj = Object.new + def obj.to_param + "foo" + end + @cache.write(obj, "bar") + assert_equal "bar", @cache.read("foo") end -end -class FileStoreTest < ActiveSupport::TestCase - def setup - @cache = ActiveSupport::Cache.lookup_store(:file_store, Dir.pwd) + def test_array_as_cache_key + @cache.write([:fu, "foo"], "bar") + assert_equal "bar", @cache.read("fu/foo") end - def teardown - File.delete("foo.cache") + def test_hash_as_cache_key + @cache.write({:foo => 1, :fu => 2}, "bar") + assert_equal "bar", @cache.read("foo=1/fu=2") end - include CacheStoreBehavior - - def test_expires_in - time = Time.local(2008, 4, 24) - Time.stubs(:now).returns(time) - File.stubs(:mtime).returns(time) + def test_keys_are_case_sensitive + @cache.write("foo", "bar") + assert_nil @cache.read("FOO") + end + def test_exist @cache.write('foo', 'bar') - cache_read = lambda { @cache.read('foo', :expires_in => 60) } - assert_equal 'bar', cache_read.call - - Time.stubs(:now).returns(time + 30) - assert_equal 'bar', cache_read.call - - Time.stubs(:now).returns(time + 120) - assert_nil cache_read.call + assert_equal true, @cache.exist?('foo') + assert_equal false, @cache.exist?('bar') end -end -class MemoryStoreTest < ActiveSupport::TestCase - def setup - @cache = ActiveSupport::Cache.lookup_store(:memory_store) + def test_nil_exist + @cache.write('foo', nil) + assert_equal true, @cache.exist?('foo') end - include CacheStoreBehavior + def test_delete + @cache.write('foo', 'bar') + assert @cache.exist?('foo') + assert_equal true, @cache.delete('foo') + assert !@cache.exist?('foo') + end def test_store_objects_should_be_immutable @cache.write('foo', 'bar') @@ -186,175 +234,365 @@ class MemoryStoreTest < ActiveSupport::TestCase assert_nothing_raised { bar.gsub!(/.*/, 'baz') } end - def test_multi_get - @cache.write('foo', 1) - @cache.write('goo', 2) - result = @cache.read_multi('foo', 'goo') - assert_equal({'foo' => 1, 'goo' => 2}, result) + def test_expires_in + time = Time.local(2008, 4, 24) + Time.stubs(:now).returns(time) + + @cache.write('foo', 'bar') + assert_equal 'bar', @cache.read('foo') + + Time.stubs(:now).returns(time + 30) + assert_equal 'bar', @cache.read('foo') + + Time.stubs(:now).returns(time + 61) + assert_nil @cache.read('foo') end -end -uses_memcached 'memcached backed store' do - class MemCacheStoreTest < ActiveSupport::TestCase - def setup - @cache = ActiveSupport::Cache.lookup_store(:mem_cache_store) - @data = @cache.instance_variable_get(:@data) - @cache.clear - @cache.silence! - @cache.logger = Logger.new("/dev/null") + def test_race_condition_protection + time = Time.now + @cache.write('foo', 'bar', :expires_in => 60) + Time.stubs(:now).returns(time + 61) + result = @cache.fetch('foo', :race_condition_ttl => 10) do + assert_equal 'bar', @cache.read('foo') + "baz" end + assert_equal "baz", result + end - include CacheStoreBehavior + def test_race_condition_protection_is_limited + time = Time.now + @cache.write('foo', 'bar', :expires_in => 60) + Time.stubs(:now).returns(time + 71) + result = @cache.fetch('foo', :race_condition_ttl => 10) do + assert_equal nil, @cache.read('foo') + "baz" + end + assert_equal "baz", result + end - def test_store_objects_should_be_immutable - @cache.with_local_cache do - @cache.write('foo', 'bar') - @cache.read('foo').gsub!(/.*/, 'baz') + def test_race_condition_protection_is_safe + time = Time.now + @cache.write('foo', 'bar', :expires_in => 60) + Time.stubs(:now).returns(time + 61) + begin + @cache.fetch('foo', :race_condition_ttl => 10) do assert_equal 'bar', @cache.read('foo') + raise ArgumentError.new end + rescue ArgumentError => e end + assert_equal "bar", @cache.read('foo') + Time.stubs(:now).returns(time + 71) + assert_nil @cache.read('foo') + end + + def test_crazy_key_characters + crazy_key = "#/:*(<+=> )&$%@?;'\"\'`~-" + assert_equal true, @cache.write(crazy_key, "1", :raw => true) + assert_equal "1", @cache.read(crazy_key) + assert_equal "1", @cache.fetch(crazy_key) + assert_equal true, @cache.delete(crazy_key) + assert_equal "2", @cache.fetch(crazy_key, :raw => true) { "2" } + assert_equal 3, @cache.increment(crazy_key) + assert_equal 2, @cache.decrement(crazy_key) + end + + def test_really_long_keys + key = "" + 1000.times{key << "x"} + assert_equal true, @cache.write(key, "bar") + assert_equal "bar", @cache.read(key) + assert_equal "bar", @cache.fetch(key) + assert_nil @cache.read("#{key}x") + assert_equal({key => "bar"}, @cache.read_multi(key)) + assert_equal true, @cache.delete(key) + end +end - def test_stored_objects_should_not_be_frozen - @cache.with_local_cache do - @cache.write('foo', 'bar') - end - @cache.with_local_cache do - assert !@cache.read('foo').frozen? - end +module CacheDeleteMatchedBehavior + def test_delete_matched + @cache.write("foo", "bar") + @cache.write("fu", "baz") + @cache.delete_matched(/oo/) + assert_equal false, @cache.exist?("foo") + assert_equal true, @cache.exist?("fu") + end +end + +module CacheIncrementDecrementBehavior + def test_increment + @cache.write('foo', 1, :raw => true) + assert_equal 1, @cache.read('foo').to_i + assert_equal 2, @cache.increment('foo') + assert_equal 2, @cache.read('foo').to_i + assert_equal 3, @cache.increment('foo') + assert_equal 3, @cache.read('foo').to_i + end + + def test_decrement + @cache.write('foo', 3, :raw => true) + assert_equal 3, @cache.read('foo').to_i + assert_equal 2, @cache.decrement('foo') + assert_equal 2, @cache.read('foo').to_i + assert_equal 1, @cache.decrement('foo') + assert_equal 1, @cache.read('foo').to_i + end +end + +module LocalCacheBehavior + def test_local_writes_are_persistent_on_the_remote_cache + retval = @cache.with_local_cache do + @cache.write('foo', 'bar') end + assert_equal true, retval + assert_equal 'bar', @cache.read('foo') + end - def test_write_should_return_true_on_success - @cache.with_local_cache do - result = @cache.write('foo', 'bar') - assert_equal 'bar', @cache.read('foo') # make sure 'foo' was written - assert result - end + def test_clear_also_clears_local_cache + @cache.with_local_cache do + @cache.write('foo', 'bar') + @cache.clear + assert_nil @cache.read('foo') end - def test_local_writes_are_persistent_on_the_remote_cache - @cache.with_local_cache do - @cache.write('foo', 'bar') - end + assert_nil @cache.read('foo') + end + def test_local_cache_of_write + @cache.with_local_cache do + @cache.write('foo', 'bar') + @peek.delete('foo') assert_equal 'bar', @cache.read('foo') end + end - def test_clear_also_clears_local_cache - @cache.with_local_cache do - @cache.write('foo', 'bar') - @cache.clear - assert_nil @cache.read('foo') - end + def test_local_cache_of_read + @cache.write('foo', 'bar') + @cache.with_local_cache do + assert_equal 'bar', @cache.read('foo') end + end - def test_local_cache_of_read_and_write - @cache.with_local_cache do - @cache.write('foo', 'bar') - @data.flush_all # Clear remote cache - assert_equal 'bar', @cache.read('foo') - end + def test_local_cache_of_write_nil + @cache.with_local_cache do + assert true, @cache.write('foo', nil) + assert_nil @cache.read('foo') + @peek.write('foo', 'bar') + assert_nil @cache.read('foo') end + end - def test_local_cache_should_read_and_write_integer - @cache.with_local_cache do - @cache.write('foo', 1) - assert_equal 1, @cache.read('foo') - end + def test_local_cache_of_delete + @cache.with_local_cache do + @cache.write('foo', 'bar') + @cache.delete('foo') + assert_nil @cache.read('foo') end + end - def test_local_cache_of_delete - @cache.with_local_cache do - @cache.write('foo', 'bar') - @cache.delete('foo') - @data.flush_all # Clear remote cache - assert_nil @cache.read('foo') - end + def test_local_cache_of_exist + @cache.with_local_cache do + @cache.write('foo', 'bar') + @peek.delete('foo') + assert true, @cache.exist?('foo') end + end - def test_local_cache_of_exist - @cache.with_local_cache do - @cache.write('foo', 'bar') - @cache.instance_variable_set(:@data, nil) - @data.flush_all # Clear remote cache - assert @cache.exist?('foo') - end + def test_local_cache_of_increment + @cache.with_local_cache do + @cache.write('foo', 1, :raw => true) + @peek.write('foo', 2, :raw => true) + @cache.increment('foo') + assert_equal 3, @cache.read('foo') end + end - def test_local_cache_of_increment - @cache.with_local_cache do - @cache.write('foo', 1, :raw => true) - @cache.increment('foo') - @data.flush_all # Clear remote cache - assert_equal 2, @cache.read('foo', :raw => true).to_i - end + def test_local_cache_of_decrement + @cache.with_local_cache do + @cache.write('foo', 1, :raw => true) + @peek.write('foo', 3, :raw => true) + @cache.decrement('foo') + assert_equal 2, @cache.read('foo') end + end - def test_local_cache_of_decrement - @cache.with_local_cache do - @cache.write('foo', 1, :raw => true) - @cache.decrement('foo') - @data.flush_all # Clear remote cache - assert_equal 0, @cache.read('foo', :raw => true).to_i - end - end + def test_middleware + app = lambda { |env| + result = @cache.write('foo', 'bar') + assert_equal 'bar', @cache.read('foo') # make sure 'foo' was written + assert result + } + app = @cache.middleware.new(app) + app.call({}) + end +end - def test_exist_with_nulls_cached_locally - @cache.with_local_cache do - @cache.write('foo', 'bar') - @cache.delete('foo') - assert !@cache.exist?('foo') - end +class FileStoreTest < ActiveSupport::TestCase + def setup + Dir.mkdir(cache_dir) unless File.exist?(cache_dir) + @cache = ActiveSupport::Cache.lookup_store(:file_store, cache_dir, :expires_in => 60) + @peek = ActiveSupport::Cache.lookup_store(:file_store, cache_dir, :expires_in => 60) + end + + def teardown + FileUtils.rm_r(cache_dir) + end + + def cache_dir + File.join(Dir.pwd, 'tmp_cache') + end + + include CacheStoreBehavior + include LocalCacheBehavior + include CacheDeleteMatchedBehavior + include CacheIncrementDecrementBehavior + + def test_deprecated_expires_in_on_read + ActiveSupport::Deprecation.silence do + old_cache = ActiveSupport::Cache.lookup_store(:file_store, cache_dir) + + time = Time.local(2008, 4, 24) + Time.stubs(:now).returns(time) + + old_cache.write("foo", "bar") + assert_equal 'bar', old_cache.read('foo', :expires_in => 60) + + Time.stubs(:now).returns(time + 30) + assert_equal 'bar', old_cache.read('foo', :expires_in => 60) + + Time.stubs(:now).returns(time + 61) + assert_equal 'bar', old_cache.read('foo') + assert_nil old_cache.read('foo', :expires_in => 60) + assert_nil old_cache.read('foo') end + end +end - def test_multi_get - @cache.with_local_cache do - @cache.write('foo', 1) - @cache.write('goo', 2) - result = @cache.read_multi('foo', 'goo') - assert_equal({'foo' => 1, 'goo' => 2}, result) - end +class MemoryStoreTest < ActiveSupport::TestCase + def setup + @cache = ActiveSupport::Cache.lookup_store(:memory_store, :expires_in => 60, :size => 100) + end + + include CacheStoreBehavior + include CacheDeleteMatchedBehavior + include CacheIncrementDecrementBehavior + + def test_prune_size + @cache.write(1, "aaaaaaaaaa") && sleep(0.001) + @cache.write(2, "bbbbbbbbbb") && sleep(0.001) + @cache.write(3, "cccccccccc") && sleep(0.001) + @cache.write(4, "dddddddddd") && sleep(0.001) + @cache.write(5, "eeeeeeeeee") && sleep(0.001) + @cache.read(2) && sleep(0.001) + @cache.read(4) + @cache.prune(30) + assert_equal true, @cache.exist?(5) + assert_equal true, @cache.exist?(4) + assert_equal false, @cache.exist?(3) + assert_equal true, @cache.exist?(2) + assert_equal false, @cache.exist?(1) + end + + def test_prune_size_on_write + @cache.write(1, "aaaaaaaaaa") && sleep(0.001) + @cache.write(2, "bbbbbbbbbb") && sleep(0.001) + @cache.write(3, "cccccccccc") && sleep(0.001) + @cache.write(4, "dddddddddd") && sleep(0.001) + @cache.write(5, "eeeeeeeeee") && sleep(0.001) + @cache.write(6, "ffffffffff") && sleep(0.001) + @cache.write(7, "gggggggggg") && sleep(0.001) + @cache.write(8, "hhhhhhhhhh") && sleep(0.001) + @cache.write(9, "iiiiiiiiii") && sleep(0.001) + @cache.write(10, "kkkkkkkkkk") && sleep(0.001) + @cache.read(2) && sleep(0.001) + @cache.read(4) && sleep(0.001) + @cache.write(11, "llllllllll") + assert_equal true, @cache.exist?(11) + assert_equal true, @cache.exist?(10) + assert_equal true, @cache.exist?(9) + assert_equal true, @cache.exist?(8) + assert_equal true, @cache.exist?(7) + assert_equal false, @cache.exist?(6) + assert_equal false, @cache.exist?(5) + assert_equal true, @cache.exist?(4) + assert_equal false, @cache.exist?(3) + assert_equal true, @cache.exist?(2) + assert_equal false, @cache.exist?(1) + end + + def test_pruning_is_capped_at_a_max_time + def @cache.delete_entry (*args) + sleep(0.01) + super end + @cache.write(1, "aaaaaaaaaa") && sleep(0.001) + @cache.write(2, "bbbbbbbbbb") && sleep(0.001) + @cache.write(3, "cccccccccc") && sleep(0.001) + @cache.write(4, "dddddddddd") && sleep(0.001) + @cache.write(5, "eeeeeeeeee") && sleep(0.001) + @cache.prune(30, 0.001) + assert_equal true, @cache.exist?(5) + assert_equal true, @cache.exist?(4) + assert_equal true, @cache.exist?(3) + assert_equal true, @cache.exist?(2) + assert_equal false, @cache.exist?(1) + end +end - def test_middleware - app = lambda { |env| - result = @cache.write('foo', 'bar') - assert_equal 'bar', @cache.read('foo') # make sure 'foo' was written - assert result - } - app = @cache.middleware.new(app) - app.call({}) +class SynchronizedStoreTest < ActiveSupport::TestCase + def setup + ActiveSupport::Deprecation.silence do + @cache = ActiveSupport::Cache.lookup_store(:memory_store, :expires_in => 60) end + end - def test_expires_in - result = @cache.write('foo', 'bar', :expires_in => 1) - assert_equal 'bar', @cache.read('foo') - sleep 2 - assert_equal nil, @cache.read('foo') + include CacheStoreBehavior + include CacheDeleteMatchedBehavior + include CacheIncrementDecrementBehavior +end + +uses_memcached 'memcached backed store' do + class MemCacheStoreTest < ActiveSupport::TestCase + def setup + @cache = ActiveSupport::Cache.lookup_store(:mem_cache_store, :expires_in => 60) + @peek = ActiveSupport::Cache.lookup_store(:mem_cache_store) + @data = @cache.instance_variable_get(:@data) + @cache.clear + @cache.silence! + @cache.logger = Logger.new("/dev/null") end - def test_expires_in_with_invalid_value - @cache.write('baz', 'bat') - assert_raise(RuntimeError) do - @cache.write('foo', 'bar', :expires_in => 'Mon Jun 29 13:10:40 -0700 2150') - end - assert_equal 'bat', @cache.read('baz') - assert_equal nil, @cache.read('foo') + include CacheStoreBehavior + include LocalCacheBehavior + include CacheIncrementDecrementBehavior + + def test_raw_values + cache = ActiveSupport::Cache.lookup_store(:mem_cache_store, :raw => true) + cache.clear + cache.write("foo", 2) + assert_equal "2", cache.read("foo") end - def test_delete_should_only_pass_key_to_data - key = 'foo' - @data.expects(:delete).with(key) - @cache.delete(key) + def test_local_cache_raw_values + cache = ActiveSupport::Cache.lookup_store(:mem_cache_store, :raw => true) + cache.clear + cache.with_local_cache do + cache.write("foo", 2) + assert_equal "2", cache.read("foo") + end end end class CompressedMemCacheStore < ActiveSupport::TestCase def setup - @cache = ActiveSupport::Cache.lookup_store(:compressed_mem_cache_store) - @cache.clear + ActiveSupport::Deprecation.silence do + @cache = ActiveSupport::Cache.lookup_store(:compressed_mem_cache_store, :expires_in => 60) + @cache.clear + end end include CacheStoreBehavior + include CacheIncrementDecrementBehavior end end @@ -376,3 +614,38 @@ class CacheStoreLoggerTest < ActiveSupport::TestCase assert @buffer.string.blank? end end + +class CacheEntryTest < ActiveSupport::TestCase + def test_create_raw_entry + time = Time.now + entry = ActiveSupport::Cache::Entry.create("raw", time, :compress => false, :expires_in => 300) + assert_equal "raw", entry.raw_value + assert_equal time.to_f, entry.created_at + assert_equal false, entry.compressed? + assert_equal 300, entry.expires_in + end + + def test_expired + entry = ActiveSupport::Cache::Entry.new("value") + assert_equal false, entry.expired? + entry = ActiveSupport::Cache::Entry.new("value", :expires_in => 60) + assert_equal false, entry.expired? + time = Time.now + 61 + Time.stubs(:now).returns(time) + assert_equal true, entry.expired? + end + + def test_compress_values + entry = ActiveSupport::Cache::Entry.new("value", :compress => true, :compress_threshold => 1) + assert_equal "value", entry.value + assert_equal true, entry.compressed? + assert_equal "value", Marshal.load(Zlib::Inflate.inflate(entry.raw_value)) + end + + def test_non_compress_values + entry = ActiveSupport::Cache::Entry.new("value") + assert_equal "value", entry.value + assert_equal "value", entry.raw_value + assert_equal false, entry.compressed? + end +end diff --git a/activesupport/test/configurable_test.rb b/activesupport/test/configurable_test.rb new file mode 100644 index 0000000000..cef67e3cf9 --- /dev/null +++ b/activesupport/test/configurable_test.rb @@ -0,0 +1,42 @@ +require 'abstract_unit' +require 'active_support/configurable' + +class ConfigurableActiveSupport < ActiveSupport::TestCase + class Parent + include ActiveSupport::Configurable + config_accessor :foo + end + + class Child < Parent + end + + setup do + Parent.config.clear + Parent.config.foo = :bar + + Child.config.clear + end + + test "adds a configuration hash" do + assert_equal({ :foo => :bar }, Parent.config) + end + + test "configuration hash is inheritable" do + assert_equal :bar, Child.config.foo + assert_equal :bar, Parent.config.foo + + Child.config.foo = :baz + assert_equal :baz, Child.config.foo + assert_equal :bar, Parent.config.foo + end + + test "configuration hash is available on instance" do + instance = Parent.new + assert_equal :bar, instance.config.foo + assert_equal :bar, Parent.config.foo + + instance.config.foo = :baz + assert_equal :baz, instance.config.foo + assert_equal :bar, Parent.config.foo + end +end
\ No newline at end of file diff --git a/activesupport/test/core_ext/hash_ext_test.rb b/activesupport/test/core_ext/hash_ext_test.rb index 86272a28c1..b2a9731578 100644 --- a/activesupport/test/core_ext/hash_ext_test.rb +++ b/activesupport/test/core_ext/hash_ext_test.rb @@ -60,6 +60,43 @@ class HashExtTest < Test::Unit::TestCase assert_equal @strings, @mixed.dup.stringify_keys! end + def test_symbolize_keys_for_hash_with_indifferent_access + assert_instance_of Hash, @symbols.with_indifferent_access.symbolize_keys + assert_equal @symbols, @symbols.with_indifferent_access.symbolize_keys + assert_equal @symbols, @strings.with_indifferent_access.symbolize_keys + assert_equal @symbols, @mixed.with_indifferent_access.symbolize_keys + end + + def test_symbolize_keys_bang_for_hash_with_indifferent_access + assert_raise(NoMethodError) { @symbols.with_indifferent_access.dup.symbolize_keys! } + assert_raise(NoMethodError) { @strings.with_indifferent_access.dup.symbolize_keys! } + assert_raise(NoMethodError) { @mixed.with_indifferent_access.dup.symbolize_keys! } + end + + def test_symbolize_keys_preserves_keys_that_cant_be_symbolized_for_hash_with_indifferent_access + assert_equal @illegal_symbols, @illegal_symbols.with_indifferent_access.symbolize_keys + assert_raise(NoMethodError) { @illegal_symbols.with_indifferent_access.dup.symbolize_keys! } + end + + def test_symbolize_keys_preserves_fixnum_keys_for_hash_with_indifferent_access + assert_equal @fixnums, @fixnums.with_indifferent_access.symbolize_keys + assert_raise(NoMethodError) { @fixnums.with_indifferent_access.dup.symbolize_keys! } + end + + def test_stringify_keys_for_hash_with_indifferent_access + assert_instance_of ActiveSupport::HashWithIndifferentAccess, @symbols.with_indifferent_access.stringify_keys + assert_equal @strings, @symbols.with_indifferent_access.stringify_keys + assert_equal @strings, @strings.with_indifferent_access.stringify_keys + assert_equal @strings, @mixed.with_indifferent_access.stringify_keys + end + + def test_stringify_keys_bang_for_hash_with_indifferent_access + assert_instance_of ActiveSupport::HashWithIndifferentAccess, @symbols.with_indifferent_access.dup.stringify_keys! + assert_equal @strings, @symbols.with_indifferent_access.dup.stringify_keys! + assert_equal @strings, @strings.with_indifferent_access.dup.stringify_keys! + assert_equal @strings, @mixed.with_indifferent_access.dup.stringify_keys! + end + def test_indifferent_assorted @strings = @strings.with_indifferent_access @symbols = @symbols.with_indifferent_access @@ -213,11 +250,11 @@ class HashExtTest < Test::Unit::TestCase def test_stringify_and_symbolize_keys_on_indifferent_preserves_hash h = HashWithIndifferentAccess.new h[:first] = 1 - h.stringify_keys! + h = h.stringify_keys assert_equal 1, h['first'] h = HashWithIndifferentAccess.new h['first'] = 1 - h.symbolize_keys! + h = h.symbolize_keys assert_equal 1, h[:first] end diff --git a/activesupport/test/core_ext/string_ext_test.rb b/activesupport/test/core_ext/string_ext_test.rb index 58ca215970..97b08da0e4 100644 --- a/activesupport/test/core_ext/string_ext_test.rb +++ b/activesupport/test/core_ext/string_ext_test.rb @@ -117,17 +117,20 @@ class StringInflectionsTest < Test::Unit::TestCase assert_equal Time.local(2005, 2, 27, 23, 50, 19, 275038), "2005-02-27T23:50:19.275038".to_time(:local) assert_equal DateTime.civil(2039, 2, 27, 23, 50), "2039-02-27 23:50".to_time assert_equal Time.local_time(2039, 2, 27, 23, 50), "2039-02-27 23:50".to_time(:local) + assert_equal nil, "".to_time end - + def test_string_to_datetime assert_equal DateTime.civil(2039, 2, 27, 23, 50), "2039-02-27 23:50".to_datetime assert_equal 0, "2039-02-27 23:50".to_datetime.offset # use UTC offset assert_equal ::Date::ITALY, "2039-02-27 23:50".to_datetime.start # use Ruby's default start value assert_equal DateTime.civil(2039, 2, 27, 23, 50, 19 + Rational(275038, 1000000), "-04:00"), "2039-02-27T23:50:19.275038-04:00".to_datetime + assert_equal nil, "".to_datetime end - + def test_string_to_date assert_equal Date.new(2005, 2, 27), "2005-02-27".to_date + assert_equal nil, "".to_date end def test_access @@ -255,7 +258,7 @@ end string.rb - Interpolation for String. Copyright (C) 2005-2009 Masao Mutoh - + You may redistribute it and/or modify it under the same license terms as Ruby. =end diff --git a/activesupport/test/core_ext/time_with_zone_test.rb b/activesupport/test/core_ext/time_with_zone_test.rb index d88f79ae4f..a808a25821 100644 --- a/activesupport/test/core_ext/time_with_zone_test.rb +++ b/activesupport/test/core_ext/time_with_zone_test.rb @@ -823,9 +823,12 @@ class TimeWithZoneMethodsForTimeAndDateTimeTest < Test::Unit::TestCase assert_equal(-18_000, Time.zone.utc_offset) end - def test_time_zone_setter_with_non_identifying_argument_returns_nil + def test_time_zone_setter_with_invalid_zone Time.zone = 'foo' - assert_equal nil, Time.zone + assert_not_nil Time.zone + assert_equal 'foo', Time.zone.name + assert_raise(TZInfo::InvalidTimezoneIdentifier) { Time.zone.utc_offset } + Time.zone = -15.hours assert_equal nil, Time.zone end diff --git a/activesupport/test/core_ext/uri_ext_test.rb b/activesupport/test/core_ext/uri_ext_test.rb index e4a242abc4..d988837603 100644 --- a/activesupport/test/core_ext/uri_ext_test.rb +++ b/activesupport/test/core_ext/uri_ext_test.rb @@ -1,3 +1,4 @@ +# encoding: utf-8 require 'abstract_unit' require 'uri' require 'active_support/core_ext/uri' @@ -5,7 +6,6 @@ require 'active_support/core_ext/uri' class URIExtTest < Test::Unit::TestCase def test_uri_decode_handle_multibyte str = "\xE6\x97\xA5\xE6\x9C\xAC\xE8\xAA\x9E" # Ni-ho-nn-go in UTF-8, means Japanese. - str.force_encoding(Encoding::UTF_8) if str.respond_to?(:force_encoding) if URI.const_defined?(:Parser) parser = URI::Parser.new diff --git a/activesupport/test/json/encoding_test.rb b/activesupport/test/json/encoding_test.rb index 188b799f3f..ac7ca96c4d 100644 --- a/activesupport/test/json/encoding_test.rb +++ b/activesupport/test/json/encoding_test.rb @@ -9,6 +9,12 @@ class TestJSONEncoding < Test::Unit::TestCase end end + class Hashlike + def to_hash + { :a => 1 } + end + end + class Custom def as_json(options) 'custom' @@ -19,7 +25,8 @@ class TestJSONEncoding < Test::Unit::TestCase FalseTests = [[ false, %(false) ]] NilTests = [[ nil, %(null) ]] NumericTests = [[ 1, %(1) ], - [ 2.5, %(2.5) ]] + [ 2.5, %(2.5) ], + [ BigDecimal('2.5'), %("#{BigDecimal('2.5').to_s('F')}") ]] StringTests = [[ 'this is the <string>', %("this is the \\u003Cstring\\u003E")], [ 'a "string" with quotes & an ampersand', %("a \\"string\\" with quotes \\u0026 an ampersand") ], @@ -35,11 +42,12 @@ class TestJSONEncoding < Test::Unit::TestCase [ :"a b", %("a b") ]] ObjectTests = [[ Foo.new(1, 2), %({\"a\":1,\"b\":2}) ]] + HashlikeTests = [[ Hashlike.new, %({\"a\":1}) ]] CustomTests = [[ Custom.new, '"custom"' ]] VariableTests = [[ ActiveSupport::JSON::Variable.new('foo'), 'foo'], [ ActiveSupport::JSON::Variable.new('alert("foo")'), 'alert("foo")']] - RegexpTests = [[ /^a/, '/^a/' ], [/^\w{1,2}[a-z]+/ix, '/^\\w{1,2}[a-z]+/ix']] + RegexpTests = [[ /^a/, '"(?-mix:^a)"' ], [/^\w{1,2}[a-z]+/ix, '"(?ix-m:^\\\\w{1,2}[a-z]+)"']] DateTests = [[ Date.new(2005,2,1), %("2005/02/01") ]] TimeTests = [[ Time.utc(2005,2,1,15,15,10), %("2005/02/01 15:15:10 +0000") ]] @@ -91,6 +99,15 @@ class TestJSONEncoding < Test::Unit::TestCase end end + if '1.9'.respond_to?(:force_encoding) + def test_non_utf8_string_transcodes + s = '二'.encode('Shift_JIS') + result = ActiveSupport::JSON.encode(s) + assert_equal '"\\u4e8c"', result + assert_equal Encoding::UTF_8, result.encoding + end + end + def test_exception_raised_when_encoding_circular_reference a = [1] a << a @@ -109,7 +126,7 @@ class TestJSONEncoding < Test::Unit::TestCase def test_hash_should_allow_key_filtering_with_except assert_equal %({"b":2}), ActiveSupport::JSON.encode({'foo' => 'bar', :b => 2, :c => 3}, :except => ['foo', :c]) end - + def test_time_to_json_includes_local_offset ActiveSupport.use_standard_json_time_format = true with_env_tz 'US/Eastern' do @@ -136,7 +153,7 @@ class TestJSONEncoding < Test::Unit::TestCase def object_keys(json_object) json_object[1..-2].scan(/([^{}:,\s]+):/).flatten.sort end - + def with_env_tz(new_tz = 'US/Eastern') old_tz, ENV['TZ'] = ENV['TZ'], new_tz yield diff --git a/activesupport/test/multibyte_chars_test.rb b/activesupport/test/multibyte_chars_test.rb index 1b8d13c024..caf50aa1c9 100644 --- a/activesupport/test/multibyte_chars_test.rb +++ b/activesupport/test/multibyte_chars_test.rb @@ -105,7 +105,7 @@ class MultibyteCharsUTF8BehaviourTest < Test::Unit::TestCase @whitespace = "\n\t#{[32, 8195].pack('U*')}" else # Ruby 1.9 only supports basic whitespace - @whitespace = "\n\t ".force_encoding(Encoding::UTF_8) + @whitespace = "\n\t " end @byte_order_mark = [65279].pack('U') diff --git a/activesupport/test/notifications_test.rb b/activesupport/test/notifications_test.rb index 92fbe5b92f..c2e1c588f0 100644 --- a/activesupport/test/notifications_test.rb +++ b/activesupport/test/notifications_test.rb @@ -6,7 +6,9 @@ module Notifications ActiveSupport::Notifications.notifier = nil @notifier = ActiveSupport::Notifications.notifier @events = [] + @named_events = [] @subscription = @notifier.subscribe { |*args| @events << event(*args) } + @named_subscription = @notifier.subscribe("named.subscription") { |*args| @named_events << event(*args) } end private @@ -30,6 +32,26 @@ module Notifications assert_equal [[:foo]], @events end + def test_unsubscribing_by_name_removes_a_subscription + @notifier.publish "named.subscription", :foo + @notifier.wait + assert_equal [["named.subscription", :foo]], @named_events + @notifier.unsubscribe("named.subscription") + @notifier.publish "named.subscription", :foo + @notifier.wait + assert_equal [["named.subscription", :foo]], @named_events + end + + def test_unsubscribing_by_name_leaves_the_other_subscriptions + @notifier.publish "named.subscription", :foo + @notifier.wait + assert_equal [["named.subscription", :foo]], @events + @notifier.unsubscribe("named.subscription") + @notifier.publish "named.subscription", :foo + @notifier.wait + assert_equal [["named.subscription", :foo], ["named.subscription", :foo]], @events + end + private def event(*args) args diff --git a/activesupport/test/time_zone_test.rb b/activesupport/test/time_zone_test.rb index 3b7fbb7808..516da7a14c 100644 --- a/activesupport/test/time_zone_test.rb +++ b/activesupport/test/time_zone_test.rb @@ -268,16 +268,16 @@ class TimeZoneTest < Test::Unit::TestCase end def test_index - assert_nil ActiveSupport::TimeZone["bogus"] + assert_not_nil ActiveSupport::TimeZone["bogus"] assert_instance_of ActiveSupport::TimeZone, ActiveSupport::TimeZone["Central Time (US & Canada)"] assert_instance_of ActiveSupport::TimeZone, ActiveSupport::TimeZone[8] assert_raise(ArgumentError) { ActiveSupport::TimeZone[false] } end - def test_unknown_zone_shouldnt_have_tzinfo_nor_utc_offset + def test_unknown_zone_should_have_tzinfo_but_exception_on_utc_offset zone = ActiveSupport::TimeZone.create("bogus") - assert_nil zone.tzinfo - assert_nil zone.utc_offset + assert_instance_of TZInfo::TimezoneProxy, zone.tzinfo + assert_raise(TZInfo::InvalidTimezoneIdentifier) { zone.utc_offset } end def test_unknown_zone_with_utc_offset diff --git a/railties/Rakefile b/railties/Rakefile index d88036f829..daffd8ce30 100644 --- a/railties/Rakefile +++ b/railties/Rakefile @@ -6,6 +6,7 @@ require 'rake/gempackagetask' require 'date' require 'rbconfig' + task :default => :test task :test => 'test:isolated' diff --git a/railties/lib/rails/application.rb b/railties/lib/rails/application.rb index 38a5aa8ca3..7cec14c738 100644 --- a/railties/lib/rails/application.rb +++ b/railties/lib/rails/application.rb @@ -112,15 +112,15 @@ module Rails def load_tasks initialize_tasks - super railties.all { |r| r.load_tasks } + super self end def load_generators initialize_generators - super railties.all { |r| r.load_generators } + super self end diff --git a/railties/lib/rails/application/configuration.rb b/railties/lib/rails/application/configuration.rb index 11bf6a6e72..874b3a78b6 100644 --- a/railties/lib/rails/application/configuration.rb +++ b/railties/lib/rails/application/configuration.rb @@ -128,13 +128,13 @@ module Rails end end - protected - def session_options return @session_options unless @session_store == :cookie_store @session_options.merge(:secret => @secret_token) end + protected + def default_middleware_stack ActionDispatch::MiddlewareStack.new.tap do |middleware| middleware.use('::ActionDispatch::Static', lambda { paths.public.to_a.first }, :if => lambda { serve_static_assets }) diff --git a/railties/lib/rails/commands/dbconsole.rb b/railties/lib/rails/commands/dbconsole.rb index 68982b9f52..8957f11724 100644 --- a/railties/lib/rails/commands/dbconsole.rb +++ b/railties/lib/rails/commands/dbconsole.rb @@ -1,6 +1,7 @@ require 'erb' require 'yaml' require 'optparse' +require 'rbconfig' module Rails class DBConsole @@ -41,7 +42,7 @@ module Rails def find_cmd(*commands) dirs_on_path = ENV['PATH'].to_s.split(File::PATH_SEPARATOR) - commands += commands.map{|cmd| "#{cmd}.exe"} if RUBY_PLATFORM =~ /win32/ + commands += commands.map{|cmd| "#{cmd}.exe"} if Config::CONFIG['host_os'] =~ /mswin|mingw/ full_path_command = nil found = commands.detect do |cmd| diff --git a/railties/lib/rails/commands/runner.rb b/railties/lib/rails/commands/runner.rb index 5634ee0f69..1dd11e1241 100644 --- a/railties/lib/rails/commands/runner.rb +++ b/railties/lib/rails/commands/runner.rb @@ -1,4 +1,5 @@ require 'optparse' +require 'rbconfig' options = { :environment => (ENV['RAILS_ENV'] || "development").dup } code_or_file = nil @@ -18,7 +19,7 @@ ARGV.clone.options do |opts| opts.on("-h", "--help", "Show this help message.") { $stderr.puts opts; exit } - if RUBY_PLATFORM !~ /mswin|mingw/ + if Config::CONFIG['host_os'] !~ /mswin|mingw/ opts.separator "" opts.separator "You can also use runner as a shebang line for your scripts like this:" opts.separator "-------------------------------------------------------------" diff --git a/railties/lib/rails/engine.rb b/railties/lib/rails/engine.rb index 0f33b40a13..36fcc896ae 100644 --- a/railties/lib/rails/engine.rb +++ b/railties/lib/rails/engine.rb @@ -1,6 +1,7 @@ require 'rails/railtie' require 'active_support/core_ext/module/delegation' require 'pathname' +require 'rbconfig' module Rails # Rails::Engine allows you to wrap a specific Rails application and share it accross @@ -119,7 +120,7 @@ module Rails root = File.exist?("#{root_path}/#{flag}") ? root_path : default raise "Could not find root path for #{self}" unless root - RUBY_PLATFORM =~ /mswin|mingw/ ? + Config::CONFIG['host_os'] =~ /mswin|mingw/ ? Pathname.new(root).expand_path : Pathname.new(root).realpath end end @@ -166,7 +167,7 @@ module Rails paths.app.controllers.to_a.each do |load_path| load_path = File.expand_path(load_path) Dir["#{load_path}/*/**/*_controller.rb"].collect do |path| - namespace = File.dirname(path).sub(/#{load_path}\/?/, '') + namespace = File.dirname(path).sub(/#{Regexp.escape(load_path)}\/?/, '') app.routes.controller_namespaces << namespace unless namespace.empty? end end diff --git a/railties/lib/rails/generators/actions.rb b/railties/lib/rails/generators/actions.rb index 7dec4d446a..a31932906d 100644 --- a/railties/lib/rails/generators/actions.rb +++ b/railties/lib/rails/generators/actions.rb @@ -1,5 +1,6 @@ require 'open-uri' require 'active_support/deprecation' +require 'rbconfig' module Rails module Generators @@ -240,7 +241,7 @@ module Rails def rake(command, options={}) log :rake, command env = options[:env] || 'development' - sudo = options[:sudo] && RUBY_PLATFORM !~ /mswin|mingw/ ? 'sudo ' : '' + sudo = options[:sudo] && Config::CONFIG['host_os'] !~ /mswin|mingw/ ? 'sudo ' : '' in_root { run("#{sudo}#{extify(:rake)} #{command} RAILS_ENV=#{env}", :verbose => false) } end @@ -307,7 +308,7 @@ module Rails # Add an extension to the given name based on the platform. # def extify(name) - if RUBY_PLATFORM =~ /mswin|mingw/ + if Config::CONFIG['host_os'] =~ /mswin|mingw/ "#{name}.bat" else name diff --git a/railties/lib/rails/generators/erb/scaffold/templates/show.html.erb b/railties/lib/rails/generators/erb/scaffold/templates/show.html.erb index 4dd2e6bf8c..6b3518717a 100644 --- a/railties/lib/rails/generators/erb/scaffold/templates/show.html.erb +++ b/railties/lib/rails/generators/erb/scaffold/templates/show.html.erb @@ -1,4 +1,4 @@ -<p class="notice"><%%= notice %></p> +<p id="notice"><%%= notice %></p> <% for attribute in attributes -%> <p> diff --git a/railties/lib/rails/generators/rails/app/app_generator.rb b/railties/lib/rails/generators/rails/app/app_generator.rb index 6818fafbe9..aa066fe3c4 100644 --- a/railties/lib/rails/generators/rails/app/app_generator.rb +++ b/railties/lib/rails/generators/rails/app/app_generator.rb @@ -1,6 +1,7 @@ require 'digest/md5' require 'active_support/secure_random' require 'rails/version' unless defined?(Rails::VERSION) +require 'rbconfig' module Rails::Generators # We need to store the RAILS_DEV_PATH in a constant, otherwise the path @@ -265,7 +266,7 @@ module Rails::Generators "/opt/local/var/run/mysql4/mysqld.sock", # mac + darwinports + mysql4 "/opt/local/var/run/mysql5/mysqld.sock", # mac + darwinports + mysql5 "/opt/lampp/var/mysql/mysql.sock" # xampp for linux - ].find { |f| File.exist?(f) } unless RUBY_PLATFORM =~ /mswin|mingw/ + ].find { |f| File.exist?(f) } unless Config::CONFIG['host_os'] =~ /mswin|mingw/ end def empty_directory_with_gitkeep(destination, config = {}) diff --git a/railties/lib/rails/generators/rails/app/templates/Rakefile b/railties/lib/rails/generators/rails/app/templates/Rakefile index 9cb2046439..13f1f9fa41 100755 --- a/railties/lib/rails/generators/rails/app/templates/Rakefile +++ b/railties/lib/rails/generators/rails/app/templates/Rakefile @@ -2,9 +2,6 @@ # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. require File.expand_path('../config/application', __FILE__) - require 'rake' -require 'rake/testtask' -require 'rake/rdoctask' Rails::Application.load_tasks diff --git a/railties/lib/rails/generators/rails/app/templates/config/boot.rb b/railties/lib/rails/generators/rails/app/templates/config/boot.rb index 62a8ccc273..ab6cb374de 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/boot.rb +++ b/railties/lib/rails/generators/rails/app/templates/config/boot.rb @@ -2,8 +2,12 @@ require 'rubygems' # Set up gems listed in the Gemfile. gemfile = File.expand_path('../../Gemfile', __FILE__) -if File.exist?(gemfile) +begin ENV['BUNDLE_GEMFILE'] = gemfile require 'bundler' Bundler.setup -end
\ No newline at end of file +rescue Bundler::GemNotFound => e + STDERR.puts e.message + STDERR.puts "Try running `bundle install`." + exit! +end if File.exist?(gemfile) diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/postgresql.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/postgresql.yml index f600e054cf..4e6391e3d6 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/databases/postgresql.yml +++ b/railties/lib/rails/generators/rails/app/templates/config/databases/postgresql.yml @@ -1,11 +1,11 @@ # PostgreSQL. Versions 7.4 and 8.x are supported. # -# Install the ruby-postgres driver: -# gem install ruby-postgres -# On Mac OS X: -# gem install ruby-postgres -- --include=/usr/local/pgsql +# Install the pg driver: +# gem install pg +# On Mac OS X with macports: +# gem install pg -- --with-pg-config=/opt/local/lib/postgresql84/bin/pg_config # On Windows: -# gem install ruby-postgres +# gem install pg # Choose the win32 build. # Install PostgreSQL and put its /bin directory on your path. development: diff --git a/railties/lib/rails/generators/rails/stylesheets/templates/scaffold.css b/railties/lib/rails/generators/rails/stylesheets/templates/scaffold.css index 9f2056a702..1ae7000299 100644 --- a/railties/lib/rails/generators/rails/stylesheets/templates/scaffold.css +++ b/railties/lib/rails/generators/rails/stylesheets/templates/scaffold.css @@ -20,15 +20,11 @@ div.field, div.actions { margin-bottom: 10px; } -.notice { +#notice { color: green; } -.alert { - color: red; -} - -.fieldWithErrors { +.field_with_errors { padding: 2px; background-color: red; display: table; diff --git a/railties/lib/rails/tasks/documentation.rake b/railties/lib/rails/tasks/documentation.rake index 957c375f6a..19d1fd2354 100644 --- a/railties/lib/rails/tasks/documentation.rake +++ b/railties/lib/rails/tasks/documentation.rake @@ -1,3 +1,5 @@ +require 'rake/rdoctask' + namespace :doc do def gem_path(gem_name) path = $LOAD_PATH.grep(/#{gem_name}[\w.-]*\/lib$/).first diff --git a/railties/lib/rails/test_help.rb b/railties/lib/rails/test_help.rb index 3ce4e2c780..ec5e4a357c 100644 --- a/railties/lib/rails/test_help.rb +++ b/railties/lib/rails/test_help.rb @@ -35,7 +35,9 @@ class ActionController::TestCase end class ActionDispatch::IntegrationTest - include Rails.application.routes.url_helpers + setup do + @routes = Rails.application.routes + end end begin diff --git a/railties/lib/rails/test_unit/testing.rake b/railties/lib/rails/test_unit/testing.rake index 83f25506cb..79fa667ed1 100644 --- a/railties/lib/rails/test_unit/testing.rake +++ b/railties/lib/rails/test_unit/testing.rake @@ -1,3 +1,5 @@ +require 'rake/testtask' + TEST_CHANGES_SINCE = Time.now - 600 # Look up tests for recently modified sources. @@ -30,7 +32,7 @@ end module Kernel def silence_stderr old_stderr = STDERR.dup - STDERR.reopen(RUBY_PLATFORM =~ /mswin|mingw/ ? 'NUL:' : '/dev/null') + STDERR.reopen(Config::CONFIG['host_os'] =~ /mswin|mingw/ ? 'NUL:' : '/dev/null') STDERR.sync = true yield ensure diff --git a/railties/test/application/configuration_test.rb b/railties/test/application/configuration_test.rb index 0f3bc1a46a..dfc4e2359b 100644 --- a/railties/test/application/configuration_test.rb +++ b/railties/test/application/configuration_test.rb @@ -109,7 +109,7 @@ module ApplicationTests end end - test "Frameworks are not preloaded by default" do + test "frameworks are not preloaded by default" do require "#{app_path}/config/environment" assert ActionController.autoload?(:RecordIdentifier) @@ -193,71 +193,10 @@ module ApplicationTests assert_equal File.join(app_path, "somewhere"), Rails.public_path end - def make_basic_app - require "rails" - require "action_controller/railtie" - - app = Class.new(Rails::Application) - - yield app if block_given? - - app.config.session_store :disabled - app.initialize! - - app.routes.draw do - match "/" => "omg#index" - end - - require 'rack/test' - extend Rack::Test::Methods - end - - test "config.action_dispatch.x_sendfile_header defaults to ''" do - make_basic_app - - class ::OmgController < ActionController::Base - def index - send_file __FILE__ - end - end - - get "/" - assert_equal File.read(__FILE__), last_response.body - end - - test "config.action_dispatch.x_sendfile_header can be set" do - make_basic_app do |app| - app.config.action_dispatch.x_sendfile_header = "X-Sendfile" - end - - class ::OmgController < ActionController::Base - def index - send_file __FILE__ - end - end - - get "/" - assert_equal File.expand_path(__FILE__), last_response.headers["X-Sendfile"] - end - - test "config.action_dispatch.x_sendfile_header is sent to Rack::Sendfile" do - make_basic_app do |app| - app.config.action_dispatch.x_sendfile_header = 'X-Lighttpd-Send-File' - end - - class ::OmgController < ActionController::Base - def index - send_file __FILE__ - end - end - - get "/" - assert_equal File.expand_path(__FILE__), last_response.headers["X-Lighttpd-Send-File"] - end - test "config.secret_token is sent in env" do make_basic_app do |app| app.config.secret_token = 'ThisIsASECRET123' + app.config.session_store :disabled end class ::OmgController < ActionController::Base @@ -287,14 +226,17 @@ module ApplicationTests end test "config.action_controller.perform_caching = true" do - make_basic_app do |app| - app.config.action_controller.perform_caching = true - end + make_basic_app do |app| + app.config.action_controller.perform_caching = true + end class ::OmgController < ActionController::Base + @@count = 0 + caches_action :index def index - render :text => rand(1000) + @@count += 1 + render :text => @@count end end @@ -310,9 +252,12 @@ module ApplicationTests end class ::OmgController < ActionController::Base + @@count = 0 + caches_action :index def index - render :text => rand(1000) + @@count += 1 + render :text => @@count end end diff --git a/railties/test/application/middleware_stack_defaults_test.rb b/railties/test/application/middleware_stack_defaults_test.rb deleted file mode 100644 index f31ca01fbf..0000000000 --- a/railties/test/application/middleware_stack_defaults_test.rb +++ /dev/null @@ -1,54 +0,0 @@ -require 'isolation/abstract_unit' - -class MiddlewareStackDefaultsTest < Test::Unit::TestCase - include ActiveSupport::Testing::Isolation - - def setup - boot_rails - require "rails" - require "action_controller/railtie" - - Object.const_set(:MyApplication, Class.new(Rails::Application)) - MyApplication.class_eval do - config.secret_token = "3b7cd727ee24e8444053437c36cc66c4" - config.session_store :cookie_store, :key => "_myapp_session" - end - end - - def remote_ip(env = {}) - remote_ip = nil - env = Rack::MockRequest.env_for("/").merge(env).merge('action_dispatch.show_exceptions' => false) - - endpoint = Proc.new do |e| - remote_ip = ActionDispatch::Request.new(e).remote_ip - [200, {}, ["Hello"]] - end - - out = MyApplication.middleware.build(endpoint).call(env) - remote_ip - end - - test "remote_ip works" do - assert_equal "1.1.1.1", remote_ip("REMOTE_ADDR" => "1.1.1.1") - end - - test "checks IP spoofing by default" do - assert_raises(ActionDispatch::RemoteIp::IpSpoofAttackError) do - remote_ip("HTTP_X_FORWARDED_FOR" => "1.1.1.1", "HTTP_CLIENT_IP" => "1.1.1.2") - end - end - - test "can disable IP spoofing check" do - MyApplication.config.action_dispatch.ip_spoofing_check = false - - assert_nothing_raised(ActionDispatch::RemoteIp::IpSpoofAttackError) do - assert_equal "1.1.1.2", remote_ip("HTTP_X_FORWARDED_FOR" => "1.1.1.1", "HTTP_CLIENT_IP" => "1.1.1.2") - end - end - - test "the user can set trusted proxies" do - MyApplication.config.action_dispatch.trusted_proxies = /^4\.2\.42\.42$/ - - assert_equal "1.1.1.1", remote_ip("REMOTE_ADDR" => "4.2.42.42,1.1.1.1") - end -end diff --git a/railties/test/application/middleware_test.rb b/railties/test/application/middleware_test.rb index 7f72881d55..27374dcb28 100644 --- a/railties/test/application/middleware_test.rb +++ b/railties/test/application/middleware_test.rb @@ -10,6 +10,10 @@ module ApplicationTests FileUtils.rm_rf "#{app_path}/config/environments" end + def app + @app ||= Rails.application + end + test "default middleware stack" do boot! @@ -83,7 +87,83 @@ module ApplicationTests assert middleware.include?("ActionDispatch::Cascade") end + # x_sendfile_header middleware + test "config.action_dispatch.x_sendfile_header defaults to ''" do + make_basic_app + + class ::OmgController < ActionController::Base + def index + send_file __FILE__ + end + end + + get "/" + assert_equal File.read(__FILE__), last_response.body + end + + test "config.action_dispatch.x_sendfile_header can be set" do + make_basic_app do |app| + app.config.action_dispatch.x_sendfile_header = "X-Sendfile" + end + + class ::OmgController < ActionController::Base + def index + send_file __FILE__ + end + end + + get "/" + assert_equal File.expand_path(__FILE__), last_response.headers["X-Sendfile"] + end + + test "config.action_dispatch.x_sendfile_header is sent to Rack::Sendfile" do + make_basic_app do |app| + app.config.action_dispatch.x_sendfile_header = 'X-Lighttpd-Send-File' + end + + class ::OmgController < ActionController::Base + def index + send_file __FILE__ + end + end + + get "/" + assert_equal File.expand_path(__FILE__), last_response.headers["X-Lighttpd-Send-File"] + end + + # remote_ip tests + test "remote_ip works" do + make_basic_app + assert_equal "1.1.1.1", remote_ip("REMOTE_ADDR" => "1.1.1.1") + end + + test "checks IP spoofing by default" do + make_basic_app + assert_raises(ActionDispatch::RemoteIp::IpSpoofAttackError) do + remote_ip("HTTP_X_FORWARDED_FOR" => "1.1.1.1", "HTTP_CLIENT_IP" => "1.1.1.2") + end + end + + test "can disable IP spoofing check" do + make_basic_app do |app| + app.config.action_dispatch.ip_spoofing_check = false + end + + assert_nothing_raised(ActionDispatch::RemoteIp::IpSpoofAttackError) do + assert_equal "1.1.1.2", remote_ip("HTTP_X_FORWARDED_FOR" => "1.1.1.1", "HTTP_CLIENT_IP" => "1.1.1.2") + end + end + + test "the user can set trusted proxies" do + make_basic_app do |app| + app.config.action_dispatch.trusted_proxies = /^4\.2\.42\.42$/ + end + + assert_equal "1.1.1.1", remote_ip("REMOTE_ADDR" => "4.2.42.42,1.1.1.1") + end + private + def boot! require "#{app_path}/config/environment" end @@ -91,5 +171,18 @@ module ApplicationTests def middleware AppTemplate::Application.middleware.active.map(&:klass).map(&:name) end + + def remote_ip(env = {}) + remote_ip = nil + env = Rack::MockRequest.env_for("/").merge(env).merge('action_dispatch.show_exceptions' => false) + + endpoint = Proc.new do |e| + remote_ip = ActionDispatch::Request.new(e).remote_ip + [200, {}, ["Hello"]] + end + + Rails.application.middleware.build(endpoint).call(env) + remote_ip + end end end diff --git a/railties/test/application/rake_test.rb b/railties/test/application/rake_test.rb new file mode 100644 index 0000000000..bf2da866f4 --- /dev/null +++ b/railties/test/application/rake_test.rb @@ -0,0 +1,23 @@ +require "isolation/abstract_unit" + +module ApplicationTests + class RakeTest < Test::Unit::TestCase + include ActiveSupport::Testing::Isolation + + def setup + build_app + boot_rails + FileUtils.rm_rf("#{app_path}/config/environments") + end + + def test_gems_tasks_are_loaded_first_than_application_ones + app_file "lib/tasks/app.rake", <<-RUBY + $task_loaded = Rake::Task.task_defined?("db:create:all") + RUBY + + require "#{app_path}/config/environment" + ::Rails.application.load_tasks + assert $task_loaded + end + end +end
\ No newline at end of file diff --git a/railties/test/isolation/abstract_unit.rb b/railties/test/isolation/abstract_unit.rb index f0c64b92ba..6f4c5d77f3 100644 --- a/railties/test/isolation/abstract_unit.rb +++ b/railties/test/isolation/abstract_unit.rb @@ -103,6 +103,25 @@ module TestHelpers add_to_config 'config.secret_token = "3b7cd727ee24e8444053437c36cc66c4"; config.session_store :cookie_store, :key => "_myapp_session"' end + def make_basic_app + require "rails" + require "action_controller/railtie" + + app = Class.new(Rails::Application) + app.config.secret_token = "3b7cd727ee24e8444053437c36cc66c4" + app.config.session_store :cookie_store, :key => "_myapp_session" + + yield app if block_given? + app.initialize! + + app.routes.draw do + match "/" => "omg#index" + end + + require 'rack/test' + extend ::Rack::Test::Methods + end + class Bukkit attr_reader :path diff --git a/tools/profile b/tools/profile index 0ccef6c26c..f02f1b5057 100755 --- a/tools/profile +++ b/tools/profile @@ -13,6 +13,7 @@ Gem.source_index require 'benchmark' module RequireProfiler + private def require(file, *args) RequireProfiler.profile(file) { super } end def load(file, *args) RequireProfiler.profile(file) { super } end |