aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Gemfile3
-rw-r--r--actionmailer/lib/action_mailer/base.rb3
-rw-r--r--actionmailer/test/old_base/mail_service_test.rb2
-rw-r--r--actionpack/CHANGELOG2
-rw-r--r--actionpack/lib/abstract_controller.rb1
-rw-r--r--actionpack/lib/abstract_controller/assigns.rb21
-rw-r--r--actionpack/lib/abstract_controller/base.rb17
-rw-r--r--actionpack/lib/abstract_controller/helpers.rb1
-rw-r--r--actionpack/lib/abstract_controller/logger.rb2
-rw-r--r--actionpack/lib/action_controller/base.rb5
-rw-r--r--actionpack/lib/action_controller/caching.rb6
-rw-r--r--actionpack/lib/action_controller/caching/pages.rb8
-rw-r--r--actionpack/lib/action_controller/deprecated/base.rb48
-rw-r--r--actionpack/lib/action_controller/metal/compatibility.rb4
-rw-r--r--actionpack/lib/action_controller/metal/helpers.rb4
-rw-r--r--actionpack/lib/action_controller/metal/renderers.rb6
-rw-r--r--actionpack/lib/action_controller/metal/request_forgery_protection.rb118
-rw-r--r--actionpack/lib/action_controller/railtie.rb65
-rw-r--r--actionpack/lib/action_controller/test_case.rb260
-rw-r--r--actionpack/lib/action_dispatch/http/parameters.rb3
-rw-r--r--actionpack/lib/action_dispatch/middleware/params_parser.rb3
-rw-r--r--actionpack/lib/action_dispatch/routing/mapper.rb9
-rw-r--r--actionpack/lib/action_dispatch/routing/url_for.rb2
-rw-r--r--actionpack/lib/action_dispatch/testing/assertions/routing.rb1
-rw-r--r--actionpack/lib/action_dispatch/testing/test_process.rb1
-rw-r--r--actionpack/lib/action_view/base.rb4
-rw-r--r--actionpack/lib/action_view/helpers/active_model_helper.rb2
-rw-r--r--actionpack/lib/action_view/helpers/prototype_helper.rb2
-rw-r--r--actionpack/lib/action_view/test_case.rb2
-rw-r--r--actionpack/test/abstract_unit.rb8
-rw-r--r--actionpack/test/controller/assert_select_test.rb2
-rw-r--r--actionpack/test/controller/layout_test.rb3
-rw-r--r--actionpack/test/controller/render_json_test.rb18
-rw-r--r--actionpack/test/controller/render_xml_test.rb24
-rw-r--r--actionpack/test/controller/send_file_test.rb4
-rw-r--r--actionpack/test/controller/url_for_test.rb16
-rw-r--r--actionpack/test/dispatch/routing_test.rb10
-rw-r--r--actionpack/test/template/active_model_helper_test.rb8
-rw-r--r--actionpack/test/template/form_tag_helper_test.rb3
-rw-r--r--actionpack/test/template/text_helper_test.rb1
-rw-r--r--actionpack/test/template/url_helper_test.rb9
-rw-r--r--activemodel/lib/active_model/locale/en.yml1
-rw-r--r--activemodel/lib/active_model/serializers/xml.rb35
-rw-r--r--activemodel/lib/active_model/validations/numericality.rb29
-rw-r--r--activemodel/test/cases/validations/i18n_validation_test.rb18
-rw-r--r--activerecord/CHANGELOG4
-rwxr-xr-xactiverecord/lib/active_record/associations.rb11
-rwxr-xr-xactiverecord/lib/active_record/base.rb3
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/quoting.rb10
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb157
-rw-r--r--activerecord/lib/active_record/fixtures.rb16
-rw-r--r--activerecord/lib/active_record/locking/optimistic.rb35
-rw-r--r--activerecord/lib/active_record/nested_attributes.rb1
-rw-r--r--activerecord/lib/active_record/railtie.rb22
-rw-r--r--activerecord/lib/active_record/railties/databases.rake4
-rw-r--r--activerecord/lib/active_record/relation/finder_methods.rb1
-rw-r--r--activerecord/lib/active_record/relation/query_methods.rb4
-rw-r--r--activerecord/lib/active_record/serializers/xml_serializer.rb11
-rw-r--r--activerecord/test/cases/finder_test.rb4
-rw-r--r--activerecord/test/cases/fixtures_test.rb10
-rw-r--r--activerecord/test/cases/locking_test.rb51
-rw-r--r--activerecord/test/fixtures/admin/accounts.yml2
-rw-r--r--activerecord/test/fixtures/admin/users.yml7
-rw-r--r--activerecord/test/models/admin.rb5
-rw-r--r--activerecord/test/models/admin/account.rb3
-rw-r--r--activerecord/test/models/admin/user.rb3
-rw-r--r--activerecord/test/schema/schema.rb9
-rw-r--r--activeresource/CHANGELOG5
-rw-r--r--activeresource/lib/active_resource/base.rb75
-rw-r--r--activeresource/test/cases/base_test.rb9
-rw-r--r--activesupport/CHANGELOG39
-rw-r--r--activesupport/lib/active_support/cache.rb496
-rw-r--r--activesupport/lib/active_support/cache/compressed_mem_cache_store.rb21
-rw-r--r--activesupport/lib/active_support/cache/file_store.rb180
-rw-r--r--activesupport/lib/active_support/cache/mem_cache_store.rb184
-rw-r--r--activesupport/lib/active_support/cache/memory_store.rb156
-rw-r--r--activesupport/lib/active_support/cache/strategy/local_cache.rb154
-rw-r--r--activesupport/lib/active_support/cache/synchronized_memory_store.rb40
-rw-r--r--activesupport/lib/active_support/configurable.rb37
-rw-r--r--activesupport/lib/active_support/core_ext/kernel/reporting.rb3
-rw-r--r--activesupport/lib/active_support/core_ext/string/conversions.rb3
-rw-r--r--activesupport/lib/active_support/core_ext/uri.rb3
-rw-r--r--activesupport/lib/active_support/hash_with_indifferent_access.rb6
-rw-r--r--activesupport/lib/active_support/json/encoding.rb24
-rw-r--r--activesupport/lib/active_support/multibyte/chars.rb4
-rw-r--r--activesupport/lib/active_support/notifications/fanout.rb20
-rw-r--r--activesupport/lib/active_support/testing/isolation.rb3
-rw-r--r--activesupport/lib/active_support/testing/setup_and_teardown.rb3
-rw-r--r--activesupport/lib/active_support/values/time_zone.rb37
-rw-r--r--activesupport/test/caching_test.rb649
-rw-r--r--activesupport/test/configurable_test.rb42
-rw-r--r--activesupport/test/core_ext/hash_ext_test.rb41
-rw-r--r--activesupport/test/core_ext/string_ext_test.rb9
-rw-r--r--activesupport/test/core_ext/time_with_zone_test.rb7
-rw-r--r--activesupport/test/core_ext/uri_ext_test.rb2
-rw-r--r--activesupport/test/json/encoding_test.rb25
-rw-r--r--activesupport/test/multibyte_chars_test.rb2
-rw-r--r--activesupport/test/notifications_test.rb22
-rw-r--r--activesupport/test/time_zone_test.rb8
-rw-r--r--railties/Rakefile1
-rw-r--r--railties/lib/rails/application.rb4
-rw-r--r--railties/lib/rails/application/configuration.rb4
-rw-r--r--railties/lib/rails/commands/dbconsole.rb3
-rw-r--r--railties/lib/rails/commands/runner.rb3
-rw-r--r--railties/lib/rails/engine.rb5
-rw-r--r--railties/lib/rails/generators/actions.rb5
-rw-r--r--railties/lib/rails/generators/erb/scaffold/templates/show.html.erb2
-rw-r--r--railties/lib/rails/generators/rails/app/app_generator.rb3
-rwxr-xr-xrailties/lib/rails/generators/rails/app/templates/Rakefile3
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/boot.rb8
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/databases/postgresql.yml10
-rw-r--r--railties/lib/rails/generators/rails/stylesheets/templates/scaffold.css8
-rw-r--r--railties/lib/rails/tasks/documentation.rake2
-rw-r--r--railties/lib/rails/test_help.rb4
-rw-r--r--railties/lib/rails/test_unit/testing.rake4
-rw-r--r--railties/test/application/configuration_test.rb81
-rw-r--r--railties/test/application/middleware_stack_defaults_test.rb54
-rw-r--r--railties/test/application/middleware_test.rb93
-rw-r--r--railties/test/application/rake_test.rb23
-rw-r--r--railties/test/isolation/abstract_unit.rb19
-rwxr-xr-xtools/profile1
121 files changed, 2408 insertions, 1408 deletions
diff --git a/Gemfile b/Gemfile
index dd26a9d1c2..f5968827a1 100644
--- a/Gemfile
+++ b/Gemfile
@@ -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