aboutsummaryrefslogtreecommitdiffstats
path: root/activesupport
diff options
context:
space:
mode:
Diffstat (limited to 'activesupport')
-rw-r--r--activesupport/CHANGELOG.md85
-rw-r--r--activesupport/activesupport.gemspec7
-rwxr-xr-xactivesupport/bin/generate_tables2
-rwxr-xr-xactivesupport/bin/test2
-rw-r--r--activesupport/lib/active_support.rb1
-rw-r--r--activesupport/lib/active_support/cache.rb117
-rw-r--r--activesupport/lib/active_support/cache/file_store.rb2
-rw-r--r--activesupport/lib/active_support/cache/mem_cache_store.rb8
-rw-r--r--activesupport/lib/active_support/cache/strategy/local_cache.rb15
-rw-r--r--activesupport/lib/active_support/callbacks.rb11
-rw-r--r--activesupport/lib/active_support/core_ext.rb2
-rw-r--r--activesupport/lib/active_support/core_ext/class/attribute.rb13
-rw-r--r--activesupport/lib/active_support/core_ext/date_and_time/calculations.rb16
-rw-r--r--activesupport/lib/active_support/core_ext/date_and_time/compatibility.rb2
-rw-r--r--activesupport/lib/active_support/core_ext/module/attribute_accessors.rb43
-rw-r--r--activesupport/lib/active_support/core_ext/module/delegation.rb39
-rw-r--r--activesupport/lib/active_support/current_attributes.rb193
-rw-r--r--activesupport/lib/active_support/dependencies.rb33
-rw-r--r--activesupport/lib/active_support/deprecation/reporting.rb2
-rw-r--r--activesupport/lib/active_support/duration.rb41
-rw-r--r--activesupport/lib/active_support/i18n.rb2
-rw-r--r--activesupport/lib/active_support/i18n_railtie.rb8
-rw-r--r--activesupport/lib/active_support/inflector/methods.rb15
-rw-r--r--activesupport/lib/active_support/log_subscriber.rb9
-rw-r--r--activesupport/lib/active_support/logger_silence.rb3
-rw-r--r--activesupport/lib/active_support/message_encryptor.rb22
-rw-r--r--activesupport/lib/active_support/multibyte/unicode.rb2
-rw-r--r--activesupport/lib/active_support/number_helper.rb1
-rw-r--r--activesupport/lib/active_support/number_helper/number_to_human_converter.rb6
-rw-r--r--activesupport/lib/active_support/number_helper/number_to_rounded_converter.rb32
-rw-r--r--activesupport/lib/active_support/number_helper/rounding_helper.rb64
-rw-r--r--activesupport/lib/active_support/railtie.rb22
-rw-r--r--activesupport/lib/active_support/reloader.rb7
-rw-r--r--activesupport/lib/active_support/rescuable.rb13
-rw-r--r--activesupport/lib/active_support/testing/assertions.rb2
-rw-r--r--activesupport/test/cache/behaviors.rb7
-rw-r--r--activesupport/test/cache/behaviors/autoloading_cache_behavior.rb41
-rw-r--r--activesupport/test/cache/behaviors/cache_delete_matched_behavior.rb13
-rw-r--r--activesupport/test/cache/behaviors/cache_increment_decrement_behavior.rb21
-rw-r--r--activesupport/test/cache/behaviors/cache_store_behavior.rb329
-rw-r--r--activesupport/test/cache/behaviors/cache_store_version_behavior.rb86
-rw-r--r--activesupport/test/cache/behaviors/encoded_key_cache_behavior.rb34
-rw-r--r--activesupport/test/cache/behaviors/local_cache_behavior.rb126
-rw-r--r--activesupport/test/cache/cache_entry_test.rb28
-rw-r--r--activesupport/test/cache/cache_key_test.rb88
-rw-r--r--activesupport/test/cache/cache_store_logger_test.rb34
-rw-r--r--activesupport/test/cache/cache_store_namespace_test.rb38
-rw-r--r--activesupport/test/cache/cache_store_setting_test.rb66
-rw-r--r--activesupport/test/cache/cache_store_write_multi_test.rb60
-rw-r--r--activesupport/test/cache/local_cache_middleware_test.rb61
-rw-r--r--activesupport/test/cache/stores/file_store_test.rb128
-rw-r--r--activesupport/test/cache/stores/mem_cache_store_test.rb74
-rw-r--r--activesupport/test/cache/stores/memory_store_test.rb107
-rw-r--r--activesupport/test/cache/stores/null_store_test.rb57
-rw-r--r--activesupport/test/caching_test.rb1203
-rw-r--r--activesupport/test/core_ext/class/attribute_test.rb10
-rw-r--r--activesupport/test/core_ext/date_time_ext_test.rb22
-rw-r--r--activesupport/test/core_ext/duration_test.rb36
-rw-r--r--activesupport/test/core_ext/module/attribute_accessor_test.rb38
-rw-r--r--activesupport/test/core_ext/module_test.rb12
-rw-r--r--activesupport/test/current_attributes_test.rb96
-rw-r--r--activesupport/test/dependencies_test.rb20
-rw-r--r--activesupport/test/dependencies_test_helpers.rb2
-rw-r--r--activesupport/test/inflector_test.rb7
-rw-r--r--activesupport/test/message_encryptor_test.rb26
-rw-r--r--activesupport/test/number_helper_test.rb6
-rw-r--r--activesupport/test/rescuable_test.rb34
-rw-r--r--activesupport/test/test_case_test.rb3
-rw-r--r--activesupport/test/testing/file_fixtures_test.rb8
-rw-r--r--activesupport/test/time_travel_test.rb2
-rw-r--r--activesupport/test/xml_mini/jdom_engine_test.rb2
71 files changed, 2331 insertions, 1436 deletions
diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md
index a4fc1e34eb..ab5237488a 100644
--- a/activesupport/CHANGELOG.md
+++ b/activesupport/CHANGELOG.md
@@ -1,3 +1,87 @@
+* Default `ActiveSupport::MessageEncryptor` to use AES 256 GCM encryption.
+
+ On for new Rails 5.2 apps. Upgrading apps can find the config as a new
+ framework default.
+
+ *Assain Jaleel*
+
+* Cache: `write_multi`
+
+ Rails.cache.write_multi foo: 'bar', baz: 'qux'
+
+ Plus faster fetch_multi with stores that implement `write_multi_entries`.
+ Keys that aren't found may be written to the cache store in one shot
+ instead of separate writes.
+
+ The default implementation simply calls `write_entry` for each entry.
+ Stores may override if they're capable of one-shot bulk writes, like
+ Redis `MSET`.
+
+ *Jeremy Daer*
+
+* Add default option to module and class attribute accessors.
+
+ mattr_accessor :settings, default: {}
+
+ Works for `mattr_reader`, `mattr_writer`, `cattr_accessor`, `cattr_reader`,
+ and `cattr_writer` as well.
+
+ *Genadi Samokovarov*
+
+* Add `Date#prev_occurring` and `Date#next_occurring` to return specified next/previous occurring day of week.
+
+ *Shota Iguchi*
+
+* Add default option to `class_attribute`.
+
+ Before:
+
+ class_attribute :settings
+ self.settings = {}
+
+ Now:
+
+ class_attribute :settings, default: {}
+
+ *DHH*
+
+* `#singularize` and `#pluralize` now respect uncountables for the specified locale.
+
+ *Eilis Hamilton*
+
+* Add `ActiveSupport::CurrentAttributes` to provide a thread-isolated attributes singleton.
+ Primary use case is keeping all the per-request attributes easily available to the whole system.
+
+ *DHH*
+
+* Fix implicit coercion calculations with scalars and durations
+
+ Previously calculations where the scalar is first would be converted to a duration
+ of seconds but this causes issues with dates being converted to times, e.g:
+
+ Time.zone = "Beijing" # => Asia/Shanghai
+ date = Date.civil(2017, 5, 20) # => Mon, 20 May 2017
+ 2 * 1.day # => 172800 seconds
+ date + 2 * 1.day # => Mon, 22 May 2017 00:00:00 CST +08:00
+
+ Now the `ActiveSupport::Duration::Scalar` calculation methods will try to maintain
+ the part structure of the duration where possible, e.g:
+
+ Time.zone = "Beijing" # => Asia/Shanghai
+ date = Date.civil(2017, 5, 20) # => Mon, 20 May 2017
+ 2 * 1.day # => 2 days
+ date + 2 * 1.day # => Mon, 22 May 2017
+
+ Fixes #29160, #28970.
+
+ *Andrew White*
+
+* Add support for versioned cache entries. This enables the cache stores to recycle cache keys, greatly saving
+ on storage in cases with frequent churn. Works together with the separation of `#cache_key` and `#cache_version`
+ in Active Record and its use in Action Pack's fragment caching.
+
+ *DHH*
+
* Pass gem name and deprecation horizon to deprecation notifications.
*Willem van Bergen*
@@ -18,4 +102,5 @@
*Josh Pencheon*
+
Please check [5-1-stable](https://github.com/rails/rails/blob/5-1-stable/activesupport/CHANGELOG.md) for previous changes.
diff --git a/activesupport/activesupport.gemspec b/activesupport/activesupport.gemspec
index 08370cba85..16e912694c 100644
--- a/activesupport/activesupport.gemspec
+++ b/activesupport/activesupport.gemspec
@@ -1,4 +1,4 @@
-version = File.read(File.expand_path("../../RAILS_VERSION", __FILE__)).strip
+version = File.read(File.expand_path("../RAILS_VERSION", __dir__)).strip
Gem::Specification.new do |s|
s.platform = Gem::Platform::RUBY
@@ -20,6 +20,11 @@ Gem::Specification.new do |s|
s.rdoc_options.concat ["--encoding", "UTF-8"]
+ s.metadata = {
+ "source_code_uri" => "https://github.com/rails/rails/tree/v#{version}/activesupport",
+ "changelog_uri" => "https://github.com/rails/rails/blob/v#{version}/activesupport/CHANGELOG.md"
+ }
+
s.add_dependency "i18n", "~> 0.7"
s.add_dependency "tzinfo", "~> 1.1"
s.add_dependency "minitest", "~> 5.1"
diff --git a/activesupport/bin/generate_tables b/activesupport/bin/generate_tables
index aa36a01b5b..6f62593f14 100755
--- a/activesupport/bin/generate_tables
+++ b/activesupport/bin/generate_tables
@@ -1,7 +1,7 @@
#!/usr/bin/env ruby
begin
- $:.unshift(File.expand_path(File.dirname(__FILE__) + "/../lib"))
+ $:.unshift(File.expand_path("../lib", __dir__))
require "active_support"
rescue IOError
end
diff --git a/activesupport/bin/test b/activesupport/bin/test
index a7beb14b27..470ce93f10 100755
--- a/activesupport/bin/test
+++ b/activesupport/bin/test
@@ -1,4 +1,4 @@
#!/usr/bin/env ruby
COMPONENT_ROOT = File.expand_path("..", __dir__)
-require File.expand_path("../tools/test", COMPONENT_ROOT)
+require_relative "../../tools/test"
diff --git a/activesupport/lib/active_support.rb b/activesupport/lib/active_support.rb
index 03e3ce821a..a667fbb54a 100644
--- a/activesupport/lib/active_support.rb
+++ b/activesupport/lib/active_support.rb
@@ -32,6 +32,7 @@ module ActiveSupport
extend ActiveSupport::Autoload
autoload :Concern
+ autoload :CurrentAttributes
autoload :Dependencies
autoload :DescendantsTracker
autoload :ExecutionWrapper
diff --git a/activesupport/lib/active_support/cache.rb b/activesupport/lib/active_support/cache.rb
index 4d8c2046e8..3847d8b7ae 100644
--- a/activesupport/lib/active_support/cache.rb
+++ b/activesupport/lib/active_support/cache.rb
@@ -88,10 +88,11 @@ module ActiveSupport
private
def retrieve_cache_key(key)
case
- when key.respond_to?(:cache_key) then key.cache_key
- when key.is_a?(Array) then key.map { |element| retrieve_cache_key(element) }.to_param
- when key.respond_to?(:to_a) then retrieve_cache_key(key.to_a)
- else key.to_param
+ when key.respond_to?(:cache_key_with_version) then key.cache_key_with_version
+ when key.respond_to?(:cache_key) then key.cache_key
+ when key.is_a?(Array) then key.map { |element| retrieve_cache_key(element) }.to_param
+ when key.respond_to?(:to_a) then retrieve_cache_key(key.to_a)
+ else key.to_param
end.to_s
end
@@ -219,6 +220,10 @@ module ActiveSupport
# cache = ActiveSupport::Cache::MemoryStore.new(expires_in: 5.minutes)
# cache.write(key, value, expires_in: 1.minute) # Set a lower value for one entry
#
+ # Setting <tt>:version</tt> verifies the cache stored under <tt>name</tt>
+ # is of the same version. nil is returned on mismatches despite contents.
+ # This feature is used to support recyclable cache keys.
+ #
# Setting <tt>:race_condition_ttl</tt> is very useful in situations where
# a cache entry is used very frequently and is under heavy load. If a
# cache expires and due to heavy load several different processes will try
@@ -287,6 +292,7 @@ module ActiveSupport
instrument(:read, name, options) do |payload|
cached_entry = read_entry(key, options) unless options[:force]
entry = handle_expired_entry(cached_entry, key, options)
+ entry = nil if entry && entry.mismatched?(normalize_version(name, options))
payload[:super_operation] = :fetch if payload
payload[:hit] = !!entry if payload
end
@@ -303,21 +309,30 @@ module ActiveSupport
end
end
- # Fetches data from the cache, using the given key. If there is data in
+ # Reads data from the cache, using the given key. If there is data in
# the cache with the given key, then that data is returned. Otherwise,
# +nil+ is returned.
#
+ # Note, if data was written with the <tt>:expires_in<tt> or <tt>:version</tt> options,
+ # both of these conditions are applied before the data is returned.
+ #
# Options are passed to the underlying cache implementation.
def read(name, options = nil)
options = merged_options(options)
- key = normalize_key(name, options)
+ key = normalize_key(name, options)
+ version = normalize_version(name, options)
+
instrument(:read, name, options) do |payload|
entry = read_entry(key, options)
+
if entry
if entry.expired?
delete_entry(key, options)
payload[:hit] = false if payload
nil
+ elsif entry.mismatched?(version)
+ payload[:hit] = false if payload
+ nil
else
payload[:hit] = true if payload
entry.value
@@ -341,11 +356,15 @@ module ActiveSupport
results = {}
names.each do |name|
- key = normalize_key(name, options)
- entry = read_entry(key, options)
+ key = normalize_key(name, options)
+ version = normalize_version(name, options)
+ entry = read_entry(key, options)
+
if entry
if entry.expired?
delete_entry(key, options)
+ elsif entry.mismatched?(version)
+ # Skip mismatched versions
else
results[name] = entry.value
end
@@ -354,6 +373,19 @@ module ActiveSupport
results
end
+ # Cache Storage API to write multiple values at once.
+ def write_multi(hash, options = nil)
+ options = merged_options(options)
+
+ instrument :write_multi, hash, options do |payload|
+ entries = hash.each_with_object({}) do |(name, value), memo|
+ memo[normalize_key(name, options)] = Entry.new(value, options.merge(version: normalize_version(name, options)))
+ end
+
+ write_multi_entries entries, options
+ end
+ end
+
# Fetches data from the cache, using the given keys. If there is data in
# the cache with the given keys, then that data is returned. Otherwise,
# the supplied block is called for each key for which there was no data,
@@ -378,14 +410,15 @@ module ActiveSupport
options = names.extract_options!
options = merged_options(options)
- results = read_multi(*names, options)
- names.each_with_object({}) do |name, memo|
- memo[name] = results.fetch(name) do
- value = yield name
- write(name, value, options)
- value
+ read_multi(*names, options).tap do |results|
+ writes = {}
+
+ (names - results.keys).each do |name|
+ results[name] = writes[name] = yield(name)
end
+
+ write_multi writes, options
end
end
@@ -396,7 +429,7 @@ module ActiveSupport
options = merged_options(options)
instrument(:write, name, options) do
- entry = Entry.new(value, options)
+ entry = Entry.new(value, options.merge(version: normalize_version(name, options)))
write_entry(normalize_key(name, options), entry, options)
end
end
@@ -420,7 +453,7 @@ module ActiveSupport
instrument(:exist?, name) do
entry = read_entry(normalize_key(name, options), options)
- (entry && !entry.expired?) || false
+ (entry && !entry.expired? && !entry.mismatched?(normalize_version(name, options))) || false
end
end
@@ -466,7 +499,7 @@ module ActiveSupport
# The options hash is passed to the underlying cache implementation.
#
# All implementations may not support this method.
- def clear
+ def clear(options = nil)
raise NotImplementedError.new("#{self.class.name} does not support clear")
end
@@ -502,6 +535,14 @@ module ActiveSupport
raise NotImplementedError.new
end
+ # Writes multiple entries to the cache implementation. Subclasses MAY
+ # implement this method.
+ def write_multi_entries(hash, options)
+ hash.each do |key, entry|
+ write_entry key, entry, options
+ end
+ end
+
# Deletes an entry from the cache implementation. Subclasses must
# implement this method.
def delete_entry(key, options)
@@ -517,6 +558,16 @@ module ActiveSupport
end
end
+ # Prefixes a key with the namespace. Namespace and key will be delimited
+ # with a colon.
+ def normalize_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
+
# Expands key to be a consistent string value. Invokes +cache_key+ if
# object responds to +cache_key+. Otherwise, +to_param+ method will be
# called. If the key is a Hash, then keys will be sorted alphabetically.
@@ -537,14 +588,16 @@ module ActiveSupport
key.to_param
end
- # Prefixes a key with the namespace. Namespace and key will be delimited
- # with a colon.
- def normalize_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
+ def normalize_version(key, options = nil)
+ (options && options[:version].try(:to_param)) || expanded_version(key)
+ end
+
+ def expanded_version(key)
+ case
+ when key.respond_to?(:cache_version) then key.cache_version.to_param
+ when key.is_a?(Array) then key.map { |element| expanded_version(element) }.compact.to_param
+ when key.respond_to?(:to_a) then expanded_version(key.to_a)
+ end
end
def instrument(operation, key, options = nil)
@@ -591,13 +644,16 @@ module ActiveSupport
end
end
- # This class is used to represent cache entries. Cache entries have a value and an optional
- # expiration time. The expiration time is used to support the :race_condition_ttl option
- # on the cache.
+ # This class is used to represent cache entries. Cache entries have a value, an optional
+ # expiration time, and an optional version. The expiration time is used to support the :race_condition_ttl option
+ # on the cache. The version is used to support the :version option on the cache for rejecting
+ # mismatches.
#
# Since cache entries in most instances will be serialized, the internals of this class are highly optimized
# using short instance variable names that are lazily defined.
class Entry # :nodoc:
+ attr_reader :version
+
DEFAULT_COMPRESS_LIMIT = 16.kilobytes
# Creates a new cache entry for the specified value. Options supported are
@@ -610,6 +666,7 @@ module ActiveSupport
@value = value
end
+ @version = options[:version]
@created_at = Time.now.to_f
@expires_in = options[:expires_in]
@expires_in = @expires_in.to_f if @expires_in
@@ -619,6 +676,10 @@ module ActiveSupport
compressed? ? uncompress(@value) : @value
end
+ def mismatched?(version)
+ @version && version && @version != version
+ end
+
# Checks if the entry is expired. The +expires_in+ parameter can override
# the value set when the entry was created.
def expired?
diff --git a/activesupport/lib/active_support/cache/file_store.rb b/activesupport/lib/active_support/cache/file_store.rb
index d5c8585816..945f50a56e 100644
--- a/activesupport/lib/active_support/cache/file_store.rb
+++ b/activesupport/lib/active_support/cache/file_store.rb
@@ -27,7 +27,7 @@ module ActiveSupport
# Deletes all items from the cache. In this case it deletes all the entries in the specified
# file store directory except for .keep or .gitkeep. Be careful which directory is specified in your
# config file when using +FileStore+ because everything in that directory will be deleted.
- def clear
+ def clear(options = nil)
root_dirs = exclude_from(cache_path, EXCLUDED_DIRS + GITKEEP_FILES)
FileUtils.rm_r(root_dirs.collect { |f| File.join(cache_path, f) })
rescue Errno::ENOENT
diff --git a/activesupport/lib/active_support/cache/mem_cache_store.rb b/activesupport/lib/active_support/cache/mem_cache_store.rb
index e09cee3335..06fa9f67ad 100644
--- a/activesupport/lib/active_support/cache/mem_cache_store.rb
+++ b/activesupport/lib/active_support/cache/mem_cache_store.rb
@@ -97,12 +97,18 @@ module ActiveSupport
options = merged_options(options)
keys_to_names = Hash[names.map { |name| [normalize_key(name, options), name] }]
+
raw_values = @data.get_multi(keys_to_names.keys)
values = {}
+
raw_values.each do |key, value|
entry = deserialize_entry(value)
- values[keys_to_names[key]] = entry.value unless entry.expired?
+
+ unless entry.expired? || entry.mismatched?(normalize_version(keys_to_names[key], options))
+ values[keys_to_names[key]] = entry.value
+ end
end
+
values
end
diff --git a/activesupport/lib/active_support/cache/strategy/local_cache.rb b/activesupport/lib/active_support/cache/strategy/local_cache.rb
index 672eb2bb80..69b3a93a05 100644
--- a/activesupport/lib/active_support/cache/strategy/local_cache.rb
+++ b/activesupport/lib/active_support/cache/strategy/local_cache.rb
@@ -44,7 +44,7 @@ module ActiveSupport
yield
end
- def clear
+ def clear(options = nil)
@data.clear
end
@@ -79,15 +79,15 @@ module ActiveSupport
local_cache_key)
end
- def clear # :nodoc:
+ def clear(options = nil) # :nodoc:
return super unless cache = local_cache
- cache.clear
+ cache.clear(options)
super
end
def cleanup(options = nil) # :nodoc:
return super unless cache = local_cache
- cache.clear(options)
+ cache.clear
super
end
@@ -115,7 +115,12 @@ module ActiveSupport
end
def write_entry(key, entry, options)
- local_cache.write_entry(key, entry, options) if local_cache
+ if options[:unless_exist]
+ local_cache.delete_entry(key, options) if local_cache
+ else
+ local_cache.write_entry(key, entry, options) if local_cache
+ end
+
super
end
diff --git a/activesupport/lib/active_support/callbacks.rb b/activesupport/lib/active_support/callbacks.rb
index d771cab68b..df18c35199 100644
--- a/activesupport/lib/active_support/callbacks.rb
+++ b/activesupport/lib/active_support/callbacks.rb
@@ -62,8 +62,7 @@ module ActiveSupport
included do
extend ActiveSupport::DescendantsTracker
- class_attribute :__callbacks, instance_writer: false
- self.__callbacks ||= {}
+ class_attribute :__callbacks, instance_writer: false, default: {}
end
CALLBACK_FILTER_TYPES = [:before, :after, :around]
@@ -597,7 +596,7 @@ module ActiveSupport
Proc.new do |target, result_lambda|
terminate = true
catch(:abort) do
- result_lambda.call if result_lambda.is_a?(Proc)
+ result_lambda.call
terminate = false
end
terminate
@@ -663,8 +662,10 @@ module ActiveSupport
if options[:if].is_a?(String) || options[:unless].is_a?(String)
ActiveSupport::Deprecation.warn(<<-MSG.squish)
- Passing string to :if and :unless conditional options is deprecated
- and will be removed in Rails 5.2 without replacement.
+ Passing string to be evaluated in :if and :unless conditional
+ options is deprecated and will be removed in Rails 5.2 without
+ replacement. Pass a symbol for an instance method, or a lambda,
+ proc or block, instead.
MSG
end
diff --git a/activesupport/lib/active_support/core_ext.rb b/activesupport/lib/active_support/core_ext.rb
index f397f658f3..42e0acf66a 100644
--- a/activesupport/lib/active_support/core_ext.rb
+++ b/activesupport/lib/active_support/core_ext.rb
@@ -1,3 +1,3 @@
-(Dir["#{File.dirname(__FILE__)}/core_ext/*.rb"]).each do |path|
+Dir.glob(File.expand_path("core_ext/*.rb", __dir__)).each do |path|
require path
end
diff --git a/activesupport/lib/active_support/core_ext/class/attribute.rb b/activesupport/lib/active_support/core_ext/class/attribute.rb
index ba422f9071..8caddcd5c3 100644
--- a/activesupport/lib/active_support/core_ext/class/attribute.rb
+++ b/activesupport/lib/active_support/core_ext/class/attribute.rb
@@ -68,11 +68,16 @@ class Class
# object.setting = false # => NoMethodError
#
# To opt out of both instance methods, pass <tt>instance_accessor: false</tt>.
+ #
+ # To set a default value for the attribute, pass <tt>default:</tt>, like so:
+ #
+ # class_attribute :settings, default: {}
def class_attribute(*attrs)
options = attrs.extract_options!
- instance_reader = options.fetch(:instance_accessor, true) && options.fetch(:instance_reader, true)
- instance_writer = options.fetch(:instance_accessor, true) && options.fetch(:instance_writer, true)
+ instance_reader = options.fetch(:instance_accessor, true) && options.fetch(:instance_reader, true)
+ instance_writer = options.fetch(:instance_accessor, true) && options.fetch(:instance_writer, true)
instance_predicate = options.fetch(:instance_predicate, true)
+ default_value = options.fetch(:default, nil)
attrs.each do |name|
remove_possible_singleton_method(name)
@@ -123,6 +128,10 @@ class Class
remove_possible_method "#{name}="
attr_writer name
end
+
+ unless default_value.nil?
+ self.send("#{name}=", default_value)
+ end
end
end
end
diff --git a/activesupport/lib/active_support/core_ext/date_and_time/calculations.rb b/activesupport/lib/active_support/core_ext/date_and_time/calculations.rb
index f2ba7fdda5..e2e1d3e359 100644
--- a/activesupport/lib/active_support/core_ext/date_and_time/calculations.rb
+++ b/activesupport/lib/active_support/core_ext/date_and_time/calculations.rb
@@ -320,6 +320,22 @@ module DateAndTime
beginning_of_year..end_of_year
end
+ # Returns specific next occurring day of week
+ def next_occurring(day_of_week)
+ current_day_number = wday != 0 ? wday - 1 : 6
+ from_now = DAYS_INTO_WEEK.fetch(day_of_week) - current_day_number
+ from_now += 7 unless from_now > 0
+ since(from_now.days)
+ end
+
+ # Returns specific previous occurring day of week
+ def prev_occurring(day_of_week)
+ current_day_number = wday != 0 ? wday - 1 : 6
+ ago = current_day_number - DAYS_INTO_WEEK.fetch(day_of_week)
+ ago += 7 unless ago > 0
+ ago(ago.days)
+ end
+
private
def first_hour(date_or_time)
date_or_time.acts_like?(:time) ? date_or_time.beginning_of_day : date_or_time
diff --git a/activesupport/lib/active_support/core_ext/date_and_time/compatibility.rb b/activesupport/lib/active_support/core_ext/date_and_time/compatibility.rb
index ab80392460..2d45e16546 100644
--- a/activesupport/lib/active_support/core_ext/date_and_time/compatibility.rb
+++ b/activesupport/lib/active_support/core_ext/date_and_time/compatibility.rb
@@ -9,6 +9,6 @@ module DateAndTime
# of the receiver. For backwards compatibility we're overriding
# this behavior, but new apps will have an initializer that sets
# this to true, because the new behavior is preferred.
- mattr_accessor(:preserve_timezone, instance_writer: false) { false }
+ mattr_accessor :preserve_timezone, instance_writer: false, default: false
end
end
diff --git a/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb b/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb
index 2c24081eb9..9244cfa157 100644
--- a/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb
+++ b/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb
@@ -38,13 +38,10 @@ class Module
#
# Person.new.hair_colors # => NoMethodError
#
- #
- # Also, you can pass a block to set up the attribute with a default value.
+ # You can set a default value for the attribute.
#
# module HairColors
- # mattr_reader :hair_colors do
- # [:brown, :black, :blonde, :red]
- # end
+ # mattr_reader :hair_colors, default: [:brown, :black, :blonde, :red]
# end
#
# class Person
@@ -52,8 +49,7 @@ class Module
# end
#
# Person.new.hair_colors # => [:brown, :black, :blonde, :red]
- def mattr_reader(*syms)
- options = syms.extract_options!
+ def mattr_reader(*syms, instance_reader: true, instance_accessor: true, default: nil)
syms.each do |sym|
raise NameError.new("invalid attribute name: #{sym}") unless /\A[_A-Za-z]\w*\z/.match?(sym)
class_eval(<<-EOS, __FILE__, __LINE__ + 1)
@@ -64,14 +60,16 @@ class Module
end
EOS
- unless options[:instance_reader] == false || options[:instance_accessor] == false
+ if instance_reader && instance_accessor
class_eval(<<-EOS, __FILE__, __LINE__ + 1)
def #{sym}
@@#{sym}
end
EOS
end
- class_variable_set("@@#{sym}", yield) if block_given?
+
+ sym_default_value = (block_given? && default.nil?) ? yield : default
+ class_variable_set("@@#{sym}", sym_default_value) unless sym_default_value.nil?
end
end
alias :cattr_reader :mattr_reader
@@ -107,12 +105,10 @@ class Module
#
# Person.new.hair_colors = [:blonde, :red] # => NoMethodError
#
- # Also, you can pass a block to set up the attribute with a default value.
+ # You can set a default value for the attribute.
#
# module HairColors
- # mattr_writer :hair_colors do
- # [:brown, :black, :blonde, :red]
- # end
+ # mattr_writer :hair_colors, default: [:brown, :black, :blonde, :red]
# end
#
# class Person
@@ -120,8 +116,7 @@ class Module
# end
#
# Person.class_variable_get("@@hair_colors") # => [:brown, :black, :blonde, :red]
- def mattr_writer(*syms)
- options = syms.extract_options!
+ def mattr_writer(*syms, instance_writer: true, instance_accessor: true, default: nil)
syms.each do |sym|
raise NameError.new("invalid attribute name: #{sym}") unless /\A[_A-Za-z]\w*\z/.match?(sym)
class_eval(<<-EOS, __FILE__, __LINE__ + 1)
@@ -132,14 +127,16 @@ class Module
end
EOS
- unless options[:instance_writer] == false || options[:instance_accessor] == false
+ if instance_writer && instance_accessor
class_eval(<<-EOS, __FILE__, __LINE__ + 1)
def #{sym}=(obj)
@@#{sym} = obj
end
EOS
end
- send("#{sym}=", yield) if block_given?
+
+ sym_default_value = (block_given? && default.nil?) ? yield : default
+ send("#{sym}=", sym_default_value) unless sym_default_value.nil?
end
end
alias :cattr_writer :mattr_writer
@@ -197,12 +194,10 @@ class Module
# Person.new.hair_colors = [:brown] # => NoMethodError
# Person.new.hair_colors # => NoMethodError
#
- # Also you can pass a block to set up the attribute with a default value.
+ # You can set a default value for the attribute.
#
# module HairColors
- # mattr_accessor :hair_colors do
- # [:brown, :black, :blonde, :red]
- # end
+ # mattr_accessor :hair_colors, default: [:brown, :black, :blonde, :red]
# end
#
# class Person
@@ -210,9 +205,9 @@ class Module
# end
#
# Person.class_variable_get("@@hair_colors") # => [:brown, :black, :blonde, :red]
- def mattr_accessor(*syms, &blk)
- mattr_reader(*syms, &blk)
- mattr_writer(*syms)
+ def mattr_accessor(*syms, instance_reader: true, instance_writer: true, instance_accessor: true, default: nil, &blk)
+ mattr_reader(*syms, instance_reader: instance_reader, instance_accessor: instance_accessor, default: default, &blk)
+ mattr_writer(*syms, instance_writer: instance_writer, instance_accessor: instance_accessor, default: default)
end
alias :cattr_accessor :mattr_accessor
end
diff --git a/activesupport/lib/active_support/core_ext/module/delegation.rb b/activesupport/lib/active_support/core_ext/module/delegation.rb
index 85ab739095..13f3894e6c 100644
--- a/activesupport/lib/active_support/core_ext/module/delegation.rb
+++ b/activesupport/lib/active_support/core_ext/module/delegation.rb
@@ -219,48 +219,43 @@ class Module
# When building decorators, a common pattern may emerge:
#
# class Partition
- # def initialize(first_event)
- # @events = [ first_event ]
+ # def initialize(event)
+ # @event = event
# end
#
- # def people
- # if @events.first.detail.people.any?
- # @events.collect { |e| Array(e.detail.people) }.flatten.uniq
- # else
- # @events.collect(&:creator).uniq
- # end
+ # def person
+ # @event.detail.person || @event.creator
# end
#
# private
# def respond_to_missing?(name, include_private = false)
- # @events.respond_to?(name, include_private)
+ # @event.respond_to?(name, include_private)
# end
#
# def method_missing(method, *args, &block)
- # @events.send(method, *args, &block)
+ # @event.send(method, *args, &block)
# end
# end
#
- # With `Module#delegate_missing_to`, the above is condensed to:
+ # With <tt>Module#delegate_missing_to</tt>, the above is condensed to:
#
# class Partition
- # delegate_missing_to :@events
+ # delegate_missing_to :@event
#
- # def initialize(first_event)
- # @events = [ first_event ]
+ # def initialize(event)
+ # @event = event
# end
#
- # def people
- # if @events.first.detail.people.any?
- # @events.collect { |e| Array(e.detail.people) }.flatten.uniq
- # else
- # @events.collect(&:creator).uniq
- # end
+ # def person
+ # @event.detail.person || @event.creator
# end
# end
#
- # The target can be anything callable within the object. E.g. instance
- # variables, methods, constants and the likes.
+ # The target can be anything callable within the object, e.g. instance
+ # variables, methods, constants, etc.
+ #
+ # The delegated method must be public on the target, otherwise it will
+ # raise +NoMethodError+.
def delegate_missing_to(target)
target = target.to_s
target = "self.#{target}" if DELEGATION_RESERVED_METHOD_NAMES.include?(target)
diff --git a/activesupport/lib/active_support/current_attributes.rb b/activesupport/lib/active_support/current_attributes.rb
new file mode 100644
index 0000000000..872b0663c7
--- /dev/null
+++ b/activesupport/lib/active_support/current_attributes.rb
@@ -0,0 +1,193 @@
+module ActiveSupport
+ # Abstract super class that provides a thread-isolated attributes singleton, which resets automatically
+ # before and after each request. This allows you to keep all the per-request attributes easily
+ # available to the whole system.
+ #
+ # The following full app-like example demonstrates how to use a Current class to
+ # facilitate easy access to the global, per-request attributes without passing them deeply
+ # around everywhere:
+ #
+ # # app/models/current.rb
+ # class Current < ActiveSupport::CurrentAttributes
+ # attribute :account, :user
+ # attribute :request_id, :user_agent, :ip_address
+ #
+ # resets { Time.zone = nil }
+ #
+ # def user=(user)
+ # super
+ # self.account = user.account
+ # Time.zone = user.time_zone
+ # end
+ # end
+ #
+ # # app/controllers/concerns/authentication.rb
+ # module Authentication
+ # extend ActiveSupport::Concern
+ #
+ # included do
+ # before_action :authenticate
+ # end
+ #
+ # private
+ # def authenticate
+ # if authenticated_user = User.find_by(id: cookies.signed[:user_id])
+ # Current.user = authenticated_user
+ # else
+ # redirect_to new_session_url
+ # end
+ # end
+ # end
+ #
+ # # app/controllers/concerns/set_current_request_details.rb
+ # module SetCurrentRequestDetails
+ # extend ActiveSupport::Concern
+ #
+ # included do
+ # before_action do
+ # Current.request_id = request.uuid
+ # Current.user_agent = request.user_agent
+ # Current.ip_address = request.ip
+ # end
+ # end
+ # end
+ #
+ # class ApplicationController < ActionController::Base
+ # include Authentication
+ # include SetCurrentRequestDetails
+ # end
+ #
+ # class MessagesController < ApplicationController
+ # def create
+ # Current.account.messages.create(message_params)
+ # end
+ # end
+ #
+ # class Message < ApplicationRecord
+ # belongs_to :creator, default: -> { Current.user }
+ # after_create { |message| Event.create(record: message) }
+ # end
+ #
+ # class Event < ApplicationRecord
+ # before_create do
+ # self.request_id = Current.request_id
+ # self.user_agent = Current.user_agent
+ # self.ip_address = Current.ip_address
+ # end
+ # end
+ #
+ # A word of caution: It's easy to overdo a global singleton like Current and tangle your model as a result.
+ # Current should only be used for a few, top-level globals, like account, user, and request details.
+ # The attributes stuck in Current should be used by more or less all actions on all requests. If you start
+ # sticking controller-specific attributes in there, you're going to create a mess.
+ class CurrentAttributes
+ include ActiveSupport::Callbacks
+ define_callbacks :reset
+
+ class << self
+ # Returns singleton instance for this class in this thread. If none exists, one is created.
+ def instance
+ current_instances[name] ||= new
+ end
+
+ # Declares one or more attributes that will be given both class and instance accessor methods.
+ def attribute(*names)
+ generated_attribute_methods.module_eval do
+ names.each do |name|
+ define_method(name) do
+ attributes[name.to_sym]
+ end
+
+ define_method("#{name}=") do |attribute|
+ attributes[name.to_sym] = attribute
+ end
+ end
+ end
+
+ names.each do |name|
+ define_singleton_method(name) do
+ instance.public_send(name)
+ end
+
+ define_singleton_method("#{name}=") do |attribute|
+ instance.public_send("#{name}=", attribute)
+ end
+ end
+ end
+
+ # Calls this block after #reset is called on the instance. Used for resetting external collaborators, like Time.zone.
+ def resets(&block)
+ set_callback :reset, :after, &block
+ end
+
+ delegate :set, :reset, to: :instance
+
+ def reset_all # :nodoc:
+ current_instances.each_value(&:reset)
+ end
+
+ def clear_all # :nodoc:
+ reset_all
+ current_instances.clear
+ end
+
+ private
+ def generated_attribute_methods
+ @generated_attribute_methods ||= Module.new.tap { |mod| include mod }
+ end
+
+ def current_instances
+ Thread.current[:current_attributes_instances] ||= {}
+ end
+
+ def method_missing(name, *args, &block)
+ # Caches the method definition as a singleton method of the receiver.
+ #
+ # By letting #delegate handle it, we avoid an enclosure that'll capture args.
+ singleton_class.delegate name, to: :instance
+
+ send(name, *args, &block)
+ end
+ end
+
+ attr_accessor :attributes
+
+ def initialize
+ @attributes = {}
+ end
+
+ # Expose one or more attributes within a block. Old values are returned after the block concludes.
+ # Example demonstrating the common use of needing to set Current attributes outside the request-cycle:
+ #
+ # class Chat::PublicationJob < ApplicationJob
+ # def perform(attributes, room_number, creator)
+ # Current.set(person: creator) do
+ # Chat::Publisher.publish(attributes: attributes, room_number: room_number)
+ # end
+ # end
+ # end
+ def set(set_attributes)
+ old_attributes = compute_attributes(set_attributes.keys)
+ assign_attributes(set_attributes)
+ yield
+ ensure
+ assign_attributes(old_attributes)
+ end
+
+ # Reset all attributes. Should be called before and after actions, when used as a per-request singleton.
+ def reset
+ run_callbacks :reset do
+ self.attributes = {}
+ end
+ end
+
+ private
+ def assign_attributes(new_attributes)
+ new_attributes.each { |key, value| public_send("#{key}=", value) }
+ end
+
+ def compute_attributes(keys)
+ keys.collect { |key| [ key, public_send(key) ] }.to_h
+ end
+ end
+end
diff --git a/activesupport/lib/active_support/dependencies.rb b/activesupport/lib/active_support/dependencies.rb
index e125b657f2..3cd8f3d0ac 100644
--- a/activesupport/lib/active_support/dependencies.rb
+++ b/activesupport/lib/active_support/dependencies.rb
@@ -18,8 +18,7 @@ module ActiveSupport #:nodoc:
module Dependencies #:nodoc:
extend self
- mattr_accessor :interlock
- self.interlock = Interlock.new
+ mattr_accessor :interlock, default: Interlock.new
# :doc:
@@ -46,46 +45,37 @@ module ActiveSupport #:nodoc:
# :nodoc:
# Should we turn on Ruby warnings on the first load of dependent files?
- mattr_accessor :warnings_on_first_load
- self.warnings_on_first_load = false
+ mattr_accessor :warnings_on_first_load, default: false
# All files ever loaded.
- mattr_accessor :history
- self.history = Set.new
+ mattr_accessor :history, default: Set.new
# All files currently loaded.
- mattr_accessor :loaded
- self.loaded = Set.new
+ mattr_accessor :loaded, default: Set.new
# Stack of files being loaded.
- mattr_accessor :loading
- self.loading = []
+ mattr_accessor :loading, default: []
# Should we load files or require them?
- mattr_accessor :mechanism
- self.mechanism = ENV["NO_RELOAD"] ? :require : :load
+ mattr_accessor :mechanism, default: ENV["NO_RELOAD"] ? :require : :load
# The set of directories from which we may automatically load files. Files
# under these directories will be reloaded on each request in development mode,
# unless the directory also appears in autoload_once_paths.
- mattr_accessor :autoload_paths
- self.autoload_paths = []
+ mattr_accessor :autoload_paths, default: []
# The set of directories from which automatically loaded constants are loaded
# only once. All directories in this set must also be present in +autoload_paths+.
- mattr_accessor :autoload_once_paths
- self.autoload_once_paths = []
+ mattr_accessor :autoload_once_paths, default: []
# An array of qualified constant names that have been loaded. Adding a name
# to this array will cause it to be unloaded the next time Dependencies are
# cleared.
- mattr_accessor :autoloaded_constants
- self.autoloaded_constants = []
+ mattr_accessor :autoloaded_constants, default: []
# An array of constant names that need to be unloaded on every request. Used
# to allow arbitrary constants to be marked for unloading.
- mattr_accessor :explicitly_unloadable_constants
- self.explicitly_unloadable_constants = []
+ mattr_accessor :explicitly_unloadable_constants, default: []
# The WatchStack keeps a stack of the modules being watched as files are
# loaded. If a file in the process of being loaded (parent.rb) triggers the
@@ -175,8 +165,7 @@ module ActiveSupport #:nodoc:
end
# An internal stack used to record which constants are loaded by any block.
- mattr_accessor :constant_watch_stack
- self.constant_watch_stack = WatchStack.new
+ mattr_accessor :constant_watch_stack, default: WatchStack.new
# Module includes this module.
module ModuleConstMissing #:nodoc:
diff --git a/activesupport/lib/active_support/deprecation/reporting.rb b/activesupport/lib/active_support/deprecation/reporting.rb
index 851d8eeda1..140bdccbb3 100644
--- a/activesupport/lib/active_support/deprecation/reporting.rb
+++ b/activesupport/lib/active_support/deprecation/reporting.rb
@@ -102,7 +102,7 @@ module ActiveSupport
end
end
- RAILS_GEM_ROOT = File.expand_path("../../../../..", __FILE__) + "/"
+ RAILS_GEM_ROOT = File.expand_path("../../../..", __dir__)
def ignored_callstack(path)
path.start_with?(RAILS_GEM_ROOT) || path.start_with?(RbConfig::CONFIG["rubylibdir"])
diff --git a/activesupport/lib/active_support/duration.rb b/activesupport/lib/active_support/duration.rb
index d4424ed792..39deb2313f 100644
--- a/activesupport/lib/active_support/duration.rb
+++ b/activesupport/lib/active_support/duration.rb
@@ -37,27 +37,56 @@ module ActiveSupport
end
def +(other)
- calculate(:+, other)
+ if Duration === other
+ seconds = value + other.parts[:seconds]
+ new_parts = other.parts.merge(seconds: seconds)
+ new_value = value + other.value
+
+ Duration.new(new_value, new_parts)
+ else
+ calculate(:+, other)
+ end
end
def -(other)
- calculate(:-, other)
+ if Duration === other
+ seconds = value - other.parts[:seconds]
+ new_parts = other.parts.map { |part, other_value| [part, -other_value] }.to_h
+ new_parts = new_parts.merge(seconds: seconds)
+ new_value = value - other.value
+
+ Duration.new(new_value, new_parts)
+ else
+ calculate(:-, other)
+ end
end
def *(other)
- calculate(:*, other)
+ if Duration === other
+ new_parts = other.parts.map { |part, other_value| [part, value * other_value] }.to_h
+ new_value = value * other.value
+
+ Duration.new(new_value, new_parts)
+ else
+ calculate(:*, other)
+ end
end
def /(other)
- calculate(:/, other)
+ if Duration === other
+ new_parts = other.parts.map { |part, other_value| [part, value / other_value] }.to_h
+ new_value = new_parts.inject(0) { |total, (part, value)| total + value * Duration::PARTS_IN_SECONDS[part] }
+
+ Duration.new(new_value, new_parts)
+ else
+ calculate(:/, other)
+ end
end
private
def calculate(op, other)
if Scalar === other
Scalar.new(value.public_send(op, other.value))
- elsif Duration === other
- Duration.seconds(value).public_send(op, other)
elsif Numeric === other
Scalar.new(value.public_send(op, other))
else
diff --git a/activesupport/lib/active_support/i18n.rb b/activesupport/lib/active_support/i18n.rb
index f0408f429c..1a1f1a1257 100644
--- a/activesupport/lib/active_support/i18n.rb
+++ b/activesupport/lib/active_support/i18n.rb
@@ -10,4 +10,4 @@ end
require "active_support/lazy_load_hooks"
ActiveSupport.run_load_hooks(:i18n)
-I18n.load_path << "#{File.dirname(__FILE__)}/locale/en.yml"
+I18n.load_path << File.expand_path("locale/en.yml", __dir__)
diff --git a/activesupport/lib/active_support/i18n_railtie.rb b/activesupport/lib/active_support/i18n_railtie.rb
index b749913ee9..51fe6f3418 100644
--- a/activesupport/lib/active_support/i18n_railtie.rb
+++ b/activesupport/lib/active_support/i18n_railtie.rb
@@ -42,7 +42,7 @@ module I18n
case setting
when :railties_load_path
reloadable_paths = value
- app.config.i18n.load_path.unshift(*value.map(&:existent).flatten)
+ app.config.i18n.load_path.unshift(*value.flat_map(&:existent))
when :load_path
I18n.load_path += value
else
@@ -58,7 +58,7 @@ module I18n
directories = watched_dirs_with_extensions(reloadable_paths)
reloader = app.config.file_watcher.new(I18n.load_path.dup, directories) do
I18n.load_path.keep_if { |p| File.exist?(p) }
- I18n.load_path |= reloadable_paths.map(&:existent).flatten
+ I18n.load_path |= reloadable_paths.flat_map(&:existent)
I18n.reload!
end
@@ -66,10 +66,6 @@ module I18n
app.reloaders << reloader
app.reloader.to_run do
reloader.execute_if_updated { require_unload_lock! }
- # TODO: remove the following line as soon as the return value of
- # callbacks is ignored, that is, returning `false` does not
- # display a deprecation warning or halts the callback chain.
- true
end
reloader.execute
diff --git a/activesupport/lib/active_support/inflector/methods.rb b/activesupport/lib/active_support/inflector/methods.rb
index 1b089a7538..ff1a0cb8c7 100644
--- a/activesupport/lib/active_support/inflector/methods.rb
+++ b/activesupport/lib/active_support/inflector/methods.rb
@@ -28,7 +28,7 @@ module ActiveSupport
# pluralize('CamelOctopus') # => "CamelOctopi"
# pluralize('ley', :es) # => "leyes"
def pluralize(word, locale = :en)
- apply_inflections(word, inflections(locale).plurals)
+ apply_inflections(word, inflections(locale).plurals, locale)
end
# The reverse of #pluralize, returns the singular form of a word in a
@@ -45,7 +45,7 @@ module ActiveSupport
# singularize('CamelOctopi') # => "CamelOctopus"
# singularize('leyes', :es) # => "ley"
def singularize(word, locale = :en)
- apply_inflections(word, inflections(locale).singulars)
+ apply_inflections(word, inflections(locale).singulars, locale)
end
# Converts strings to UpperCamelCase.
@@ -387,12 +387,15 @@ module ActiveSupport
# Applies inflection rules for +singularize+ and +pluralize+.
#
- # apply_inflections('post', inflections.plurals) # => "posts"
- # apply_inflections('posts', inflections.singulars) # => "post"
- def apply_inflections(word, rules)
+ # If passed an optional +locale+ parameter, the uncountables will be
+ # found for that locale.
+ #
+ # apply_inflections('post', inflections.plurals, :en) # => "posts"
+ # apply_inflections('posts', inflections.singulars, :en) # => "post"
+ def apply_inflections(word, rules, locale = :en)
result = word.to_s.dup
- if word.empty? || inflections.uncountables.uncountable?(result)
+ if word.empty? || inflections(locale).uncountables.uncountable?(result)
result
else
rules.each { |(rule, replacement)| break if result.sub!(rule, replacement) }
diff --git a/activesupport/lib/active_support/log_subscriber.rb b/activesupport/lib/active_support/log_subscriber.rb
index e2c4f33565..a05758d6aa 100644
--- a/activesupport/lib/active_support/log_subscriber.rb
+++ b/activesupport/lib/active_support/log_subscriber.rb
@@ -49,8 +49,7 @@ module ActiveSupport
CYAN = "\e[36m"
WHITE = "\e[37m"
- mattr_accessor :colorize_logging
- self.colorize_logging = true
+ mattr_accessor :colorize_logging, default: true
class << self
def logger
@@ -81,8 +80,10 @@ module ActiveSupport
def finish(name, id, payload)
super if logger
- rescue Exception => e
- logger.error "Could not log #{name.inspect} event. #{e.class}: #{e.message} #{e.backtrace}"
+ rescue => e
+ if logger
+ logger.error "Could not log #{name.inspect} event. #{e.class}: #{e.message} #{e.backtrace}"
+ end
end
private
diff --git a/activesupport/lib/active_support/logger_silence.rb b/activesupport/lib/active_support/logger_silence.rb
index 632994cf50..9c64afaaca 100644
--- a/activesupport/lib/active_support/logger_silence.rb
+++ b/activesupport/lib/active_support/logger_silence.rb
@@ -6,8 +6,7 @@ module LoggerSilence
extend ActiveSupport::Concern
included do
- cattr_accessor :silencer
- self.silencer = true
+ cattr_accessor :silencer, default: true
end
# Silences the logger for the duration of the block.
diff --git a/activesupport/lib/active_support/message_encryptor.rb b/activesupport/lib/active_support/message_encryptor.rb
index 24053b4fe5..e576766c64 100644
--- a/activesupport/lib/active_support/message_encryptor.rb
+++ b/activesupport/lib/active_support/message_encryptor.rb
@@ -19,7 +19,17 @@ module ActiveSupport
# encrypted_data = crypt.encrypt_and_sign('my secret data') # => "NlFBTTMwOUV5UlA1QlNEN2xkY2d6eThYWWh..."
# crypt.decrypt_and_verify(encrypted_data) # => "my secret data"
class MessageEncryptor
- DEFAULT_CIPHER = "aes-256-cbc"
+ class << self
+ attr_accessor :use_authenticated_message_encryption #:nodoc:
+
+ def default_cipher #:nodoc:
+ if use_authenticated_message_encryption
+ "aes-256-gcm"
+ else
+ "aes-256-cbc"
+ end
+ end
+ end
module NullSerializer #:nodoc:
def self.load(value)
@@ -45,7 +55,7 @@ module ActiveSupport
OpenSSLCipherError = OpenSSL::Cipher::CipherError
# Initialize a new MessageEncryptor. +secret+ must be at least as long as
- # the cipher key size. For the default 'aes-256-cbc' cipher, this is 256
+ # the cipher key size. For the default 'aes-256-gcm' cipher, this is 256
# bits. If you are using a user-entered secret, you can generate a suitable
# key by using <tt>ActiveSupport::KeyGenerator</tt> or a similar key
# derivation function.
@@ -57,7 +67,7 @@ module ActiveSupport
#
# Options:
# * <tt>:cipher</tt> - Cipher to use. Can be any cipher returned by
- # <tt>OpenSSL::Cipher.ciphers</tt>. Default is 'aes-256-cbc'.
+ # <tt>OpenSSL::Cipher.ciphers</tt>. Default is 'aes-256-gcm'.
# * <tt>:digest</tt> - String of digest to use for signing. Default is
# +SHA1+. Ignored when using an AEAD cipher like 'aes-256-gcm'.
# * <tt>:serializer</tt> - Object serializer to use. Default is +Marshal+.
@@ -66,7 +76,7 @@ module ActiveSupport
sign_secret = signature_key_or_options.first
@secret = secret
@sign_secret = sign_secret
- @cipher = options[:cipher] || DEFAULT_CIPHER
+ @cipher = options[:cipher] || self.class.default_cipher
@digest = options[:digest] || "SHA1" unless aead_mode?
@verifier = resolve_verifier
@serializer = options[:serializer] || Marshal
@@ -85,7 +95,7 @@ module ActiveSupport
end
# Given a cipher, returns the key length of the cipher to help generate the key of desired size
- def self.key_len(cipher = DEFAULT_CIPHER)
+ def self.key_len(cipher = default_cipher)
OpenSSL::Cipher.new(cipher).key_len
end
@@ -115,7 +125,7 @@ module ActiveSupport
# Currently the OpenSSL bindings do not raise an error if auth_tag is
# truncated, which would allow an attacker to easily forge it. See
# https://github.com/ruby/openssl/issues/63
- raise InvalidMessage if aead_mode? && auth_tag.bytes.length != 16
+ raise InvalidMessage if aead_mode? && (auth_tag.nil? || auth_tag.bytes.length != 16)
cipher.decrypt
cipher.key = @secret
diff --git a/activesupport/lib/active_support/multibyte/unicode.rb b/activesupport/lib/active_support/multibyte/unicode.rb
index 0912912aba..8223e45e5a 100644
--- a/activesupport/lib/active_support/multibyte/unicode.rb
+++ b/activesupport/lib/active_support/multibyte/unicode.rb
@@ -357,7 +357,7 @@ module ActiveSupport
# Returns the directory in which the data files are stored.
def self.dirname
- File.dirname(__FILE__) + "/../values/"
+ File.expand_path("../values", __dir__)
end
# Returns the filename for the data file for this version.
diff --git a/activesupport/lib/active_support/number_helper.rb b/activesupport/lib/active_support/number_helper.rb
index 880340ca86..9cb2821cb6 100644
--- a/activesupport/lib/active_support/number_helper.rb
+++ b/activesupport/lib/active_support/number_helper.rb
@@ -4,6 +4,7 @@ module ActiveSupport
eager_autoload do
autoload :NumberConverter
+ autoload :RoundingHelper
autoload :NumberToRoundedConverter
autoload :NumberToDelimitedConverter
autoload :NumberToHumanConverter
diff --git a/activesupport/lib/active_support/number_helper/number_to_human_converter.rb b/activesupport/lib/active_support/number_helper/number_to_human_converter.rb
index 56185ddf4b..040343b5dd 100644
--- a/activesupport/lib/active_support/number_helper/number_to_human_converter.rb
+++ b/activesupport/lib/active_support/number_helper/number_to_human_converter.rb
@@ -9,6 +9,7 @@ module ActiveSupport
self.validate_float = true
def convert # :nodoc:
+ @number = RoundingHelper.new(options).round(number)
@number = Float(number)
# for backwards compatibility with those that didn't add strip_insignificant_zeros to their locale files
@@ -20,10 +21,7 @@ module ActiveSupport
exponent = calculate_exponent(units)
@number = number / (10**exponent)
- until (rounded_number = NumberToRoundedConverter.convert(number, options)) != NumberToRoundedConverter.convert(1000, options)
- @number = number / 1000.0
- exponent += 3
- end
+ rounded_number = NumberToRoundedConverter.convert(number, options)
unit = determine_unit(units, exponent)
format.gsub("%n".freeze, rounded_number).gsub("%u".freeze, unit).strip
end
diff --git a/activesupport/lib/active_support/number_helper/number_to_rounded_converter.rb b/activesupport/lib/active_support/number_helper/number_to_rounded_converter.rb
index 1f013990ea..c32d85a45f 100644
--- a/activesupport/lib/active_support/number_helper/number_to_rounded_converter.rb
+++ b/activesupport/lib/active_support/number_helper/number_to_rounded_converter.rb
@@ -5,26 +5,14 @@ module ActiveSupport
self.validate_float = true
def convert
- precision = options.delete :precision
+ helper = RoundingHelper.new(options)
+ rounded_number = helper.round(number)
- if precision
- case number
- when Float, String
- @number = BigDecimal(number.to_s)
- when Rational
- @number = BigDecimal(number, digit_count(number.to_i) + precision)
- else
- @number = number.to_d
- end
-
- if options.delete(:significant) && precision > 0
- digits, rounded_number = digits_and_rounded_number(precision)
+ if precision = options[:precision]
+ if options[:significant] && precision > 0
+ digits = helper.digit_count(rounded_number)
precision -= digits
precision = 0 if precision < 0 # don't let it be negative
- else
- rounded_number = number.round(precision)
- rounded_number = rounded_number.to_i if precision == 0 && rounded_number.finite?
- rounded_number = rounded_number.abs if rounded_number.zero? # prevent showing negative zeros
end
formatted_string =
@@ -38,7 +26,7 @@ module ActiveSupport
"%00.#{precision}f" % rounded_number
end
else
- formatted_string = number
+ formatted_string = rounded_number
end
delimited_number = NumberToDelimitedConverter.convert(formatted_string, options)
@@ -79,14 +67,6 @@ module ActiveSupport
number
end
end
-
- def absolute_number(number)
- number.respond_to?(:abs) ? number.abs : number.to_d.abs
- end
-
- def zero?
- number.respond_to?(:zero?) ? number.zero? : number.to_d.zero?
- end
end
end
end
diff --git a/activesupport/lib/active_support/number_helper/rounding_helper.rb b/activesupport/lib/active_support/number_helper/rounding_helper.rb
new file mode 100644
index 0000000000..63b48444a6
--- /dev/null
+++ b/activesupport/lib/active_support/number_helper/rounding_helper.rb
@@ -0,0 +1,64 @@
+module ActiveSupport
+ module NumberHelper
+ class RoundingHelper # :nodoc:
+ attr_reader :options
+
+ def initialize(options)
+ @options = options
+ end
+
+ def round(number)
+ return number unless precision
+ number = convert_to_decimal(number)
+ if significant && precision > 0
+ round_significant(number)
+ else
+ round_without_significant(number)
+ end
+ end
+
+ def digit_count(number)
+ return 1 if number.zero?
+ (Math.log10(absolute_number(number)) + 1).floor
+ end
+
+ private
+ def round_without_significant(number)
+ number = number.round(precision)
+ number = number.to_i if precision == 0 && number.finite?
+ number = number.abs if number.zero? # prevent showing negative zeros
+ number
+ end
+
+ def round_significant(number)
+ return 0 if number.zero?
+ digits = digit_count(number)
+ multiplier = 10**(digits - precision)
+ (number / BigDecimal.new(multiplier.to_f.to_s)).round * multiplier
+ end
+
+ def convert_to_decimal(number)
+ case number
+ when Float, String
+ BigDecimal(number.to_s)
+ when Rational
+ BigDecimal(number, digit_count(number.to_i) + precision)
+ else
+ number.to_d
+ end
+ end
+
+ def precision
+ options[:precision]
+ end
+
+ def significant
+ options[:significant]
+ end
+
+ def absolute_number(number)
+ number.respond_to?(:abs) ? number.abs : number.to_d.abs
+ end
+ end
+ end
+end
diff --git a/activesupport/lib/active_support/railtie.rb b/activesupport/lib/active_support/railtie.rb
index b875875afe..45bc51311b 100644
--- a/activesupport/lib/active_support/railtie.rb
+++ b/activesupport/lib/active_support/railtie.rb
@@ -7,6 +7,19 @@ module ActiveSupport
config.eager_load_namespaces << ActiveSupport
+ initializer "active_support.set_authenticated_message_encryption" do |app|
+ if app.config.active_support.respond_to?(:use_authenticated_message_encryption)
+ ActiveSupport::MessageEncryptor.use_authenticated_message_encryption =
+ app.config.active_support.use_authenticated_message_encryption
+ end
+ end
+
+ initializer "active_support.reset_all_current_attributes_instances" do |app|
+ app.reloader.before_class_unload { ActiveSupport::CurrentAttributes.clear_all }
+ app.executor.to_run { ActiveSupport::CurrentAttributes.reset_all }
+ app.executor.to_complete { ActiveSupport::CurrentAttributes.reset_all }
+ end
+
initializer "active_support.deprecation_behavior" do |app|
if deprecation = app.config.active_support.deprecation
ActiveSupport::Deprecation.behavior = deprecation
@@ -22,14 +35,7 @@ module ActiveSupport
raise e.exception "tzinfo-data is not present. Please add gem 'tzinfo-data' to your Gemfile and run bundle install"
end
require "active_support/core_ext/time/zones"
- zone_default = Time.find_zone!(app.config.time_zone)
-
- unless zone_default
- raise "Value assigned to config.time_zone not recognized. " \
- 'Run "rake time:zones:all" for a time zone names list.'
- end
-
- Time.zone_default = zone_default
+ Time.zone_default = Time.find_zone!(app.config.time_zone)
end
# Sets the default week start
diff --git a/activesupport/lib/active_support/reloader.rb b/activesupport/lib/active_support/reloader.rb
index 121c621751..9558146201 100644
--- a/activesupport/lib/active_support/reloader.rb
+++ b/activesupport/lib/active_support/reloader.rb
@@ -69,11 +69,8 @@ module ActiveSupport
end
end
- class_attribute :executor
- class_attribute :check
-
- self.executor = Executor
- self.check = lambda { false }
+ class_attribute :executor, default: Executor
+ class_attribute :check, default: lambda { false }
def self.check! # :nodoc:
@should_reload ||= check.call
diff --git a/activesupport/lib/active_support/rescuable.rb b/activesupport/lib/active_support/rescuable.rb
index ee6592fb5a..826832ba7d 100644
--- a/activesupport/lib/active_support/rescuable.rb
+++ b/activesupport/lib/active_support/rescuable.rb
@@ -8,8 +8,7 @@ module ActiveSupport
extend Concern
included do
- class_attribute :rescue_handlers
- self.rescue_handlers = []
+ class_attribute :rescue_handlers, default: []
end
module ClassMethods
@@ -84,12 +83,18 @@ module ActiveSupport
# end
#
# Returns the exception if it was handled and +nil+ if it was not.
- def rescue_with_handler(exception, object: self)
+ def rescue_with_handler(exception, object: self, visited_exceptions: [])
+ visited_exceptions << exception
+
if handler = handler_for_rescue(exception, object: object)
handler.call exception
exception
elsif exception
- rescue_with_handler(exception.cause, object: object)
+ if visited_exceptions.include?(exception.cause)
+ nil
+ else
+ rescue_with_handler(exception.cause, object: object, visited_exceptions: visited_exceptions)
+ end
end
end
diff --git a/activesupport/lib/active_support/testing/assertions.rb b/activesupport/lib/active_support/testing/assertions.rb
index 28cf2953bf..28e1df8870 100644
--- a/activesupport/lib/active_support/testing/assertions.rb
+++ b/activesupport/lib/active_support/testing/assertions.rb
@@ -167,7 +167,7 @@ module ActiveSupport
retval
end
- # Assertion that the result of evaluating an expression is changed before
+ # Assertion that the result of evaluating an expression is not changed before
# and after invoking the passed in block.
#
# assert_no_changes 'Status.all_good?' do
diff --git a/activesupport/test/cache/behaviors.rb b/activesupport/test/cache/behaviors.rb
new file mode 100644
index 0000000000..efd045ac5e
--- /dev/null
+++ b/activesupport/test/cache/behaviors.rb
@@ -0,0 +1,7 @@
+require_relative "behaviors/autoloading_cache_behavior"
+require_relative "behaviors/cache_delete_matched_behavior"
+require_relative "behaviors/cache_increment_decrement_behavior"
+require_relative "behaviors/cache_store_behavior"
+require_relative "behaviors/cache_store_version_behavior"
+require_relative "behaviors/encoded_key_cache_behavior"
+require_relative "behaviors/local_cache_behavior"
diff --git a/activesupport/test/cache/behaviors/autoloading_cache_behavior.rb b/activesupport/test/cache/behaviors/autoloading_cache_behavior.rb
new file mode 100644
index 0000000000..5f8af331f6
--- /dev/null
+++ b/activesupport/test/cache/behaviors/autoloading_cache_behavior.rb
@@ -0,0 +1,41 @@
+require "dependencies_test_helpers"
+
+module AutoloadingCacheBehavior
+ include DependenciesTestHelpers
+
+ def test_simple_autoloading
+ with_autoloading_fixtures do
+ @cache.write("foo", EM.new)
+ end
+
+ remove_constants(:EM)
+ ActiveSupport::Dependencies.clear
+
+ with_autoloading_fixtures do
+ assert_kind_of EM, @cache.read("foo")
+ end
+
+ remove_constants(:EM)
+ ActiveSupport::Dependencies.clear
+ end
+
+ def test_two_classes_autoloading
+ with_autoloading_fixtures do
+ @cache.write("foo", [EM.new, ClassFolder.new])
+ end
+
+ remove_constants(:EM, :ClassFolder)
+ ActiveSupport::Dependencies.clear
+
+ with_autoloading_fixtures do
+ loaded = @cache.read("foo")
+ assert_kind_of Array, loaded
+ assert_equal 2, loaded.size
+ assert_kind_of EM, loaded[0]
+ assert_kind_of ClassFolder, loaded[1]
+ end
+
+ remove_constants(:EM, :ClassFolder)
+ ActiveSupport::Dependencies.clear
+ end
+end
diff --git a/activesupport/test/cache/behaviors/cache_delete_matched_behavior.rb b/activesupport/test/cache/behaviors/cache_delete_matched_behavior.rb
new file mode 100644
index 0000000000..b872eb0279
--- /dev/null
+++ b/activesupport/test/cache/behaviors/cache_delete_matched_behavior.rb
@@ -0,0 +1,13 @@
+module CacheDeleteMatchedBehavior
+ def test_delete_matched
+ @cache.write("foo", "bar")
+ @cache.write("fu", "baz")
+ @cache.write("foo/bar", "baz")
+ @cache.write("fu/baz", "bar")
+ @cache.delete_matched(/oo/)
+ assert !@cache.exist?("foo")
+ assert @cache.exist?("fu")
+ assert !@cache.exist?("foo/bar")
+ assert @cache.exist?("fu/baz")
+ end
+end
diff --git a/activesupport/test/cache/behaviors/cache_increment_decrement_behavior.rb b/activesupport/test/cache/behaviors/cache_increment_decrement_behavior.rb
new file mode 100644
index 0000000000..0d32339565
--- /dev/null
+++ b/activesupport/test/cache/behaviors/cache_increment_decrement_behavior.rb
@@ -0,0 +1,21 @@
+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
+ assert_nil @cache.increment("bar")
+ 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
+ assert_nil @cache.decrement("bar")
+ end
+end
diff --git a/activesupport/test/cache/behaviors/cache_store_behavior.rb b/activesupport/test/cache/behaviors/cache_store_behavior.rb
new file mode 100644
index 0000000000..03c366e164
--- /dev/null
+++ b/activesupport/test/cache/behaviors/cache_store_behavior.rb
@@ -0,0 +1,329 @@
+# Tests the base functionality that should be identical across all cache stores.
+module CacheStoreBehavior
+ def test_should_read_and_write_strings
+ assert @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.write("foo", "bar")
+ assert_not_called(@cache, :write) do
+ assert_equal "bar", @cache.fetch("foo") { "baz" }
+ end
+ end
+
+ def test_fetch_with_cache_miss
+ assert_called_with(@cache, :write, ["foo", "baz", @cache.options]) do
+ assert_equal "baz", @cache.fetch("foo") { "baz" }
+ end
+ end
+
+ def test_fetch_with_cache_miss_passes_key_to_block
+ cache_miss = false
+ assert_equal 3, @cache.fetch("foo") { |key| cache_miss = true; key.length }
+ assert cache_miss
+
+ cache_miss = false
+ assert_equal 3, @cache.fetch("foo") { |key| cache_miss = true; key.length }
+ assert !cache_miss
+ end
+
+ def test_fetch_with_forced_cache_miss
+ @cache.write("foo", "bar")
+ assert_not_called(@cache, :read) do
+ assert_called_with(@cache, :write, ["foo", "bar", @cache.options.merge(force: true)]) do
+ @cache.fetch("foo", force: true) { "bar" }
+ end
+ end
+ end
+
+ def test_fetch_with_cached_nil
+ @cache.write("foo", nil)
+ assert_not_called(@cache, :write) do
+ assert_nil @cache.fetch("foo") { "baz" }
+ end
+ end
+
+ def test_fetch_with_forced_cache_miss_with_block
+ @cache.write("foo", "bar")
+ assert_equal "foo_bar", @cache.fetch("foo", force: true) { "foo_bar" }
+ end
+
+ def test_fetch_with_forced_cache_miss_without_block
+ @cache.write("foo", "bar")
+ assert_raises(ArgumentError) do
+ @cache.fetch("foo", force: true)
+ end
+
+ assert_equal "bar", @cache.read("foo")
+ end
+
+ def test_should_read_and_write_hash
+ assert @cache.write("foo", a: "b")
+ assert_equal({ a: "b" }, @cache.read("foo"))
+ end
+
+ def test_should_read_and_write_integer
+ assert @cache.write("foo", 1)
+ assert_equal 1, @cache.read("foo")
+ end
+
+ def test_should_read_and_write_nil
+ assert @cache.write("foo", nil)
+ assert_nil @cache.read("foo")
+ end
+
+ def test_should_read_and_write_false
+ assert @cache.write("foo", false)
+ assert_equal false, @cache.read("foo")
+ end
+
+ def test_read_multi
+ @cache.write("foo", "bar")
+ @cache.write("fu", "baz")
+ @cache.write("fud", "biz")
+ assert_equal({ "foo" => "bar", "fu" => "baz" }, @cache.read_multi("foo", "fu"))
+ end
+
+ def test_read_multi_with_expires
+ time = Time.now
+ @cache.write("foo", "bar", expires_in: 10)
+ @cache.write("fu", "baz")
+ @cache.write("fud", "biz")
+ Time.stub(:now, time + 11) do
+ assert_equal({ "fu" => "baz" }, @cache.read_multi("foo", "fu"))
+ end
+ end
+
+ def test_fetch_multi
+ @cache.write("foo", "bar")
+ @cache.write("fud", "biz")
+
+ values = @cache.fetch_multi("foo", "fu", "fud") { |value| value * 2 }
+
+ assert_equal({ "foo" => "bar", "fu" => "fufu", "fud" => "biz" }, values)
+ assert_equal("fufu", @cache.read("fu"))
+ end
+
+ def test_multi_with_objects
+ cache_struct = Struct.new(:cache_key, :title)
+ foo = cache_struct.new("foo", "FOO!")
+ bar = cache_struct.new("bar")
+
+ @cache.write("bar", "BAM!")
+
+ values = @cache.fetch_multi(foo, bar) { |object| object.title }
+
+ assert_equal({ foo => "FOO!", bar => "BAM!" }, values)
+ end
+
+ def test_fetch_multi_without_block
+ assert_raises(ArgumentError) do
+ @cache.fetch_multi("foo")
+ end
+ end
+
+ def test_read_and_write_compressed_small_data
+ @cache.write("foo", "bar", compress: true)
+ assert_equal "bar", @cache.read("foo")
+ end
+
+ def test_read_and_write_compressed_large_data
+ @cache.write("foo", "bar", compress: true, compress_threshold: 2)
+ assert_equal "bar", @cache.read("foo")
+ end
+
+ def test_read_and_write_compressed_nil
+ @cache.write("foo", nil, compress: true)
+ assert_nil @cache.read("foo")
+ end
+
+ 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_param_as_cache_key
+ obj = Object.new
+ def obj.to_param
+ "foo"
+ end
+ @cache.write(obj, "bar")
+ assert_equal "bar", @cache.read("foo")
+ end
+
+ def test_array_as_cache_key
+ @cache.write([:fu, "foo"], "bar")
+ assert_equal "bar", @cache.read("fu/foo")
+ end
+
+ def test_hash_as_cache_key
+ @cache.write({ foo: 1, fu: 2 }, "bar")
+ assert_equal "bar", @cache.read("foo=1/fu=2")
+ end
+
+ def test_keys_are_case_sensitive
+ @cache.write("foo", "bar")
+ assert_nil @cache.read("FOO")
+ end
+
+ def test_exist
+ @cache.write("foo", "bar")
+ assert_equal true, @cache.exist?("foo")
+ assert_equal false, @cache.exist?("bar")
+ end
+
+ def test_nil_exist
+ @cache.write("foo", nil)
+ assert @cache.exist?("foo")
+ end
+
+ def test_delete
+ @cache.write("foo", "bar")
+ assert @cache.exist?("foo")
+ assert @cache.delete("foo")
+ assert !@cache.exist?("foo")
+ end
+
+ def test_original_store_objects_should_not_be_immutable
+ bar = "bar"
+ @cache.write("foo", bar)
+ assert_nothing_raised { bar.gsub!(/.*/, "baz") }
+ end
+
+ def test_expires_in
+ time = Time.local(2008, 4, 24)
+
+ Time.stub(:now, time) do
+ @cache.write("foo", "bar")
+ assert_equal "bar", @cache.read("foo")
+ end
+
+ Time.stub(:now, time + 30) do
+ assert_equal "bar", @cache.read("foo")
+ end
+
+ Time.stub(:now, time + 61) do
+ assert_nil @cache.read("foo")
+ end
+ end
+
+ def test_race_condition_protection_skipped_if_not_defined
+ @cache.write("foo", "bar")
+ time = @cache.send(:read_entry, @cache.send(:normalize_key, "foo", {}), {}).expires_at
+
+ Time.stub(:now, Time.at(time)) do
+ result = @cache.fetch("foo") do
+ assert_nil @cache.read("foo")
+ "baz"
+ end
+ assert_equal "baz", result
+ end
+ end
+
+ def test_race_condition_protection_is_limited
+ time = Time.now
+ @cache.write("foo", "bar", expires_in: 60)
+ Time.stub(:now, time + 71) do
+ result = @cache.fetch("foo", race_condition_ttl: 10) do
+ assert_nil @cache.read("foo")
+ "baz"
+ end
+ assert_equal "baz", result
+ end
+ end
+
+ def test_race_condition_protection_is_safe
+ time = Time.now
+ @cache.write("foo", "bar", expires_in: 60)
+ Time.stub(:now, time + 61) do
+ begin
+ @cache.fetch("foo", race_condition_ttl: 10) do
+ assert_equal "bar", @cache.read("foo")
+ raise ArgumentError.new
+ end
+ rescue ArgumentError
+ end
+ assert_equal "bar", @cache.read("foo")
+ end
+ Time.stub(:now, time + 91) do
+ assert_nil @cache.read("foo")
+ end
+ end
+
+ def test_race_condition_protection
+ time = Time.now
+ @cache.write("foo", "bar", expires_in: 60)
+ Time.stub(:now, time + 61) do
+ result = @cache.fetch("foo", race_condition_ttl: 10) do
+ assert_equal "bar", @cache.read("foo")
+ "baz"
+ end
+ assert_equal "baz", result
+ end
+ end
+
+ def test_crazy_key_characters
+ crazy_key = "#/:*(<+=> )&$%@?;'\"\'`~-"
+ assert @cache.write(crazy_key, "1", raw: true)
+ assert_equal "1", @cache.read(crazy_key)
+ assert_equal "1", @cache.fetch(crazy_key)
+ assert @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 = ""
+ 900.times { key << "x" }
+ assert @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 @cache.delete(key)
+ end
+
+ def test_cache_hit_instrumentation
+ key = "test_key"
+ @events = []
+ ActiveSupport::Notifications.subscribe "cache_read.active_support" do |*args|
+ @events << ActiveSupport::Notifications::Event.new(*args)
+ end
+ assert @cache.write(key, "1", raw: true)
+ assert @cache.fetch(key) {}
+ assert_equal 1, @events.length
+ assert_equal "cache_read.active_support", @events[0].name
+ assert_equal :fetch, @events[0].payload[:super_operation]
+ assert @events[0].payload[:hit]
+ ensure
+ ActiveSupport::Notifications.unsubscribe "cache_read.active_support"
+ end
+
+ def test_cache_miss_instrumentation
+ @events = []
+ ActiveSupport::Notifications.subscribe(/^cache_(.*)\.active_support$/) do |*args|
+ @events << ActiveSupport::Notifications::Event.new(*args)
+ end
+ assert_not @cache.fetch("bad_key") {}
+ assert_equal 3, @events.length
+ assert_equal "cache_read.active_support", @events[0].name
+ assert_equal "cache_generate.active_support", @events[1].name
+ assert_equal "cache_write.active_support", @events[2].name
+ assert_equal :fetch, @events[0].payload[:super_operation]
+ assert_not @events[0].payload[:hit]
+ ensure
+ ActiveSupport::Notifications.unsubscribe "cache_read.active_support"
+ end
+end
diff --git a/activesupport/test/cache/behaviors/cache_store_version_behavior.rb b/activesupport/test/cache/behaviors/cache_store_version_behavior.rb
new file mode 100644
index 0000000000..a0170c896f
--- /dev/null
+++ b/activesupport/test/cache/behaviors/cache_store_version_behavior.rb
@@ -0,0 +1,86 @@
+module CacheStoreVersionBehavior
+ ModelWithKeyAndVersion = Struct.new(:cache_key, :cache_version)
+
+ def test_fetch_with_right_version_should_hit
+ @cache.fetch("foo", version: 1) { "bar" }
+ assert_equal "bar", @cache.read("foo", version: 1)
+ end
+
+ def test_fetch_with_wrong_version_should_miss
+ @cache.fetch("foo", version: 1) { "bar" }
+ assert_nil @cache.read("foo", version: 2)
+ end
+
+ def test_read_with_right_version_should_hit
+ @cache.write("foo", "bar", version: 1)
+ assert_equal "bar", @cache.read("foo", version: 1)
+ end
+
+ def test_read_with_wrong_version_should_miss
+ @cache.write("foo", "bar", version: 1)
+ assert_nil @cache.read("foo", version: 2)
+ end
+
+ def test_exist_with_right_version_should_be_true
+ @cache.write("foo", "bar", version: 1)
+ assert @cache.exist?("foo", version: 1)
+ end
+
+ def test_exist_with_wrong_version_should_be_false
+ @cache.write("foo", "bar", version: 1)
+ assert !@cache.exist?("foo", version: 2)
+ end
+
+ def test_reading_and_writing_with_model_supporting_cache_version
+ m1v1 = ModelWithKeyAndVersion.new("model/1", 1)
+ m1v2 = ModelWithKeyAndVersion.new("model/1", 2)
+
+ @cache.write(m1v1, "bar")
+ assert_equal "bar", @cache.read(m1v1)
+ assert_nil @cache.read(m1v2)
+ end
+
+ def test_reading_and_writing_with_model_supporting_cache_version_using_nested_key
+ m1v1 = ModelWithKeyAndVersion.new("model/1", 1)
+ m1v2 = ModelWithKeyAndVersion.new("model/1", 2)
+
+ @cache.write([ "something", m1v1 ], "bar")
+ assert_equal "bar", @cache.read([ "something", m1v1 ])
+ assert_nil @cache.read([ "something", m1v2 ])
+ end
+
+ def test_fetching_with_model_supporting_cache_version
+ m1v1 = ModelWithKeyAndVersion.new("model/1", 1)
+ m1v2 = ModelWithKeyAndVersion.new("model/1", 2)
+
+ @cache.fetch(m1v1) { "bar" }
+ assert_equal "bar", @cache.fetch(m1v1) { "bu" }
+ assert_equal "bu", @cache.fetch(m1v2) { "bu" }
+ end
+
+ def test_exist_with_model_supporting_cache_version
+ m1v1 = ModelWithKeyAndVersion.new("model/1", 1)
+ m1v2 = ModelWithKeyAndVersion.new("model/1", 2)
+
+ @cache.write(m1v1, "bar")
+ assert @cache.exist?(m1v1)
+ assert_not @cache.fetch(m1v2)
+ end
+
+ def test_fetch_multi_with_model_supporting_cache_version
+ m1v1 = ModelWithKeyAndVersion.new("model/1", 1)
+ m2v1 = ModelWithKeyAndVersion.new("model/2", 1)
+ m2v2 = ModelWithKeyAndVersion.new("model/2", 2)
+
+ first_fetch_values = @cache.fetch_multi(m1v1, m2v1) { |m| m.cache_key }
+ second_fetch_values = @cache.fetch_multi(m1v1, m2v2) { |m| m.cache_key + " 2nd" }
+
+ assert_equal({ m1v1 => "model/1", m2v1 => "model/2" }, first_fetch_values)
+ assert_equal({ m1v1 => "model/1", m2v2 => "model/2 2nd" }, second_fetch_values)
+ end
+
+ def test_version_is_normalized
+ @cache.write("foo", "bar", version: 1)
+ assert_equal "bar", @cache.read("foo", version: "1")
+ end
+end
diff --git a/activesupport/test/cache/behaviors/encoded_key_cache_behavior.rb b/activesupport/test/cache/behaviors/encoded_key_cache_behavior.rb
new file mode 100644
index 0000000000..4d8e2946b2
--- /dev/null
+++ b/activesupport/test/cache/behaviors/encoded_key_cache_behavior.rb
@@ -0,0 +1,34 @@
+# https://rails.lighthouseapp.com/projects/8994/tickets/6225-memcachestore-cant-deal-with-umlauts-and-special-characters
+# The error is caused by character encodings that can't be compared with ASCII-8BIT regular expressions and by special
+# characters like the umlaut in UTF-8.
+module EncodedKeyCacheBehavior
+ Encoding.list.each do |encoding|
+ define_method "test_#{encoding.name.underscore}_encoded_values" do
+ key = "foo".force_encoding(encoding)
+ assert @cache.write(key, "1", raw: true)
+ assert_equal "1", @cache.read(key)
+ assert_equal "1", @cache.fetch(key)
+ assert @cache.delete(key)
+ assert_equal "2", @cache.fetch(key, raw: true) { "2" }
+ assert_equal 3, @cache.increment(key)
+ assert_equal 2, @cache.decrement(key)
+ end
+ end
+
+ def test_common_utf8_values
+ key = "\xC3\xBCmlaut".force_encoding(Encoding::UTF_8)
+ assert @cache.write(key, "1", raw: true)
+ assert_equal "1", @cache.read(key)
+ assert_equal "1", @cache.fetch(key)
+ assert @cache.delete(key)
+ assert_equal "2", @cache.fetch(key, raw: true) { "2" }
+ assert_equal 3, @cache.increment(key)
+ assert_equal 2, @cache.decrement(key)
+ end
+
+ def test_retains_encoding
+ key = "\xC3\xBCmlaut".force_encoding(Encoding::UTF_8)
+ assert @cache.write(key, "1", raw: true)
+ assert_equal Encoding::UTF_8, key.encoding
+ end
+end
diff --git a/activesupport/test/cache/behaviors/local_cache_behavior.rb b/activesupport/test/cache/behaviors/local_cache_behavior.rb
new file mode 100644
index 0000000000..8530296374
--- /dev/null
+++ b/activesupport/test/cache/behaviors/local_cache_behavior.rb
@@ -0,0 +1,126 @@
+module LocalCacheBehavior
+ def test_local_writes_are_persistent_on_the_remote_cache
+ retval = @cache.with_local_cache do
+ @cache.write("foo", "bar")
+ end
+ assert retval
+ assert_equal "bar", @cache.read("foo")
+ 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
+
+ assert_nil @cache.read("foo")
+ end
+
+ def test_cleanup_clears_local_cache_but_not_remote_cache
+ skip unless @cache.class.instance_methods(false).include?(:cleanup)
+
+ @cache.with_local_cache do
+ @cache.write("foo", "bar")
+ assert_equal "bar", @cache.read("foo")
+
+ @cache.send(:bypass_local_cache) { @cache.write("foo", "baz") }
+ assert_equal "bar", @cache.read("foo")
+
+ @cache.cleanup
+ assert_equal "baz", @cache.read("foo")
+ end
+ 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_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_nil
+ @cache.with_local_cache do
+ assert_nil @cache.read("foo")
+ @cache.send(:bypass_local_cache) { @cache.write "foo", "bar" }
+ assert_nil @cache.read("foo")
+ end
+ end
+
+ def test_local_cache_fetch
+ @cache.with_local_cache do
+ @cache.send(:local_cache).write "foo", "bar"
+ assert_equal "bar", @cache.send(:local_cache).fetch("foo")
+ end
+ end
+
+ def test_local_cache_of_write_nil
+ @cache.with_local_cache do
+ assert @cache.write("foo", nil)
+ assert_nil @cache.read("foo")
+ @peek.write("foo", "bar")
+ assert_nil @cache.read("foo")
+ end
+ end
+
+ def test_local_cache_of_write_with_unless_exist
+ @cache.with_local_cache do
+ @cache.write("foo", "bar")
+ @cache.write("foo", "baz", unless_exist: true)
+ assert_equal @peek.read("foo"), @cache.read("foo")
+ end
+ 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_exist
+ @cache.with_local_cache do
+ @cache.write("foo", "bar")
+ @peek.delete("foo")
+ assert @cache.exist?("foo")
+ end
+ 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_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_middleware
+ app = lambda { |env|
+ result = @cache.write("foo", "bar")
+ assert_equal "bar", @cache.read("foo") # make sure 'foo' was written
+ assert result
+ [200, {}, []]
+ }
+ app = @cache.middleware.new(app)
+ app.call({})
+ end
+end
diff --git a/activesupport/test/cache/cache_entry_test.rb b/activesupport/test/cache/cache_entry_test.rb
new file mode 100644
index 0000000000..e446e39b10
--- /dev/null
+++ b/activesupport/test/cache/cache_entry_test.rb
@@ -0,0 +1,28 @@
+require "abstract_unit"
+require "active_support/cache"
+
+class CacheEntryTest < ActiveSupport::TestCase
+ def test_expired
+ entry = ActiveSupport::Cache::Entry.new("value")
+ assert !entry.expired?, "entry not expired"
+ entry = ActiveSupport::Cache::Entry.new("value", expires_in: 60)
+ assert !entry.expired?, "entry not expired"
+ Time.stub(:now, Time.now + 61) do
+ assert entry.expired?, "entry is expired"
+ end
+ end
+
+ def test_compress_values
+ value = "value" * 100
+ entry = ActiveSupport::Cache::Entry.new(value, compress: true, compress_threshold: 1)
+ assert_equal value, entry.value
+ assert(value.bytesize > entry.size, "value is compressed")
+ end
+
+ def test_non_compress_values
+ value = "value" * 100
+ entry = ActiveSupport::Cache::Entry.new(value)
+ assert_equal value, entry.value
+ assert_equal value.bytesize, entry.size
+ end
+end
diff --git a/activesupport/test/cache/cache_key_test.rb b/activesupport/test/cache/cache_key_test.rb
new file mode 100644
index 0000000000..149d0f66ee
--- /dev/null
+++ b/activesupport/test/cache/cache_key_test.rb
@@ -0,0 +1,88 @@
+require "abstract_unit"
+require "active_support/cache"
+
+class CacheKeyTest < ActiveSupport::TestCase
+ def test_entry_legacy_optional_ivars
+ legacy = Class.new(ActiveSupport::Cache::Entry) do
+ def initialize(value, options = {})
+ @value = value
+ @expires_in = nil
+ @created_at = nil
+ super
+ end
+ end
+
+ entry = legacy.new "foo"
+ assert_equal "foo", entry.value
+ end
+
+ 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
+
+ def test_expand_cache_key_with_rails_cache_id
+ with_env("RAILS_CACHE_ID" => "c99") do
+ assert_equal "c99/foo", ActiveSupport::Cache.expand_cache_key(:foo)
+ assert_equal "c99/foo", ActiveSupport::Cache.expand_cache_key([:foo])
+ assert_equal "c99/foo/bar", ActiveSupport::Cache.expand_cache_key([:foo, :bar])
+ assert_equal "nm/c99/foo", ActiveSupport::Cache.expand_cache_key(:foo, :nm)
+ assert_equal "nm/c99/foo", ActiveSupport::Cache.expand_cache_key([:foo], :nm)
+ assert_equal "nm/c99/foo/bar", ActiveSupport::Cache.expand_cache_key([:foo, :bar], :nm)
+ end
+ end
+
+ def test_expand_cache_key_with_rails_app_version
+ with_env("RAILS_APP_VERSION" => "rails3") do
+ assert_equal "rails3/foo", ActiveSupport::Cache.expand_cache_key(:foo)
+ end
+ end
+
+ def test_expand_cache_key_rails_cache_id_should_win_over_rails_app_version
+ with_env("RAILS_CACHE_ID" => "c99", "RAILS_APP_VERSION" => "rails3") do
+ assert_equal "c99/foo", ActiveSupport::Cache.expand_cache_key(:foo)
+ end
+ end
+
+ def test_expand_cache_key_respond_to_cache_key
+ key = "foo"
+ def key.cache_key
+ :foo_key
+ end
+ assert_equal "foo_key", ActiveSupport::Cache.expand_cache_key(key)
+ end
+
+ def test_expand_cache_key_array_with_something_that_responds_to_cache_key
+ key = "foo"
+ def key.cache_key
+ :foo_key
+ end
+ assert_equal "foo_key", ActiveSupport::Cache.expand_cache_key([key])
+ end
+
+ def test_expand_cache_key_of_nil
+ assert_equal "", ActiveSupport::Cache.expand_cache_key(nil)
+ end
+
+ def test_expand_cache_key_of_false
+ assert_equal "false", ActiveSupport::Cache.expand_cache_key(false)
+ end
+
+ def test_expand_cache_key_of_true
+ assert_equal "true", ActiveSupport::Cache.expand_cache_key(true)
+ end
+
+ def test_expand_cache_key_of_array_like_object
+ assert_equal "foo/bar/baz", ActiveSupport::Cache.expand_cache_key(%w{foo bar baz}.to_enum)
+ end
+
+ private
+
+ def with_env(kv)
+ old_values = {}
+ kv.each { |key, value| old_values[key], ENV[key] = ENV[key], value }
+ yield
+ ensure
+ old_values.each { |key, value| ENV[key] = value }
+ end
+end
diff --git a/activesupport/test/cache/cache_store_logger_test.rb b/activesupport/test/cache/cache_store_logger_test.rb
new file mode 100644
index 0000000000..621cfebb10
--- /dev/null
+++ b/activesupport/test/cache/cache_store_logger_test.rb
@@ -0,0 +1,34 @@
+require "abstract_unit"
+require "active_support/cache"
+
+class CacheStoreLoggerTest < ActiveSupport::TestCase
+ def setup
+ @cache = ActiveSupport::Cache.lookup_store(:memory_store)
+
+ @buffer = StringIO.new
+ @cache.logger = ActiveSupport::Logger.new(@buffer)
+ end
+
+ def test_logging
+ @cache.fetch("foo") { "bar" }
+ assert @buffer.string.present?
+ end
+
+ def test_log_with_string_namespace
+ @cache.fetch("foo", namespace: "string_namespace") { "bar" }
+ assert_match %r{string_namespace:foo}, @buffer.string
+ end
+
+ def test_log_with_proc_namespace
+ proc = Proc.new do
+ "proc_namespace"
+ end
+ @cache.fetch("foo", namespace: proc) { "bar" }
+ assert_match %r{proc_namespace:foo}, @buffer.string
+ end
+
+ def test_mute_logging
+ @cache.mute { @cache.fetch("foo") { "bar" } }
+ assert @buffer.string.blank?
+ end
+end
diff --git a/activesupport/test/cache/cache_store_namespace_test.rb b/activesupport/test/cache/cache_store_namespace_test.rb
new file mode 100644
index 0000000000..e395c88271
--- /dev/null
+++ b/activesupport/test/cache/cache_store_namespace_test.rb
@@ -0,0 +1,38 @@
+require "abstract_unit"
+require "active_support/cache"
+
+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 !cache.exist?("foo")
+ assert 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 !cache.exist?("foo")
+ assert cache.exist?("fu")
+ end
+end
diff --git a/activesupport/test/cache/cache_store_setting_test.rb b/activesupport/test/cache/cache_store_setting_test.rb
new file mode 100644
index 0000000000..cb9b006abe
--- /dev/null
+++ b/activesupport/test/cache/cache_store_setting_test.rb
@@ -0,0 +1,66 @@
+require "abstract_unit"
+require "active_support/cache"
+require "dalli"
+
+class CacheStoreSettingTest < ActiveSupport::TestCase
+ def test_memory_store_gets_created_if_no_arguments_passed_to_lookup_store_method
+ store = ActiveSupport::Cache.lookup_store
+ assert_kind_of(ActiveSupport::Cache::MemoryStore, store)
+ end
+
+ def test_memory_store
+ store = ActiveSupport::Cache.lookup_store :memory_store
+ assert_kind_of(ActiveSupport::Cache::MemoryStore, store)
+ end
+
+ def test_file_fragment_cache_store
+ store = ActiveSupport::Cache.lookup_store :file_store, "/path/to/cache/directory"
+ assert_kind_of(ActiveSupport::Cache::FileStore, store)
+ assert_equal "/path/to/cache/directory", store.cache_path
+ end
+
+ def test_mem_cache_fragment_cache_store
+ assert_called_with(Dalli::Client, :new, [%w[localhost], {}]) do
+ store = ActiveSupport::Cache.lookup_store :mem_cache_store, "localhost"
+ assert_kind_of(ActiveSupport::Cache::MemCacheStore, store)
+ end
+ end
+
+ def test_mem_cache_fragment_cache_store_with_given_mem_cache
+ mem_cache = Dalli::Client.new
+ assert_not_called(Dalli::Client, :new) do
+ store = ActiveSupport::Cache.lookup_store :mem_cache_store, mem_cache
+ assert_kind_of(ActiveSupport::Cache::MemCacheStore, store)
+ end
+ end
+
+ def test_mem_cache_fragment_cache_store_with_not_dalli_client
+ assert_not_called(Dalli::Client, :new) do
+ memcache = Object.new
+ assert_raises(ArgumentError) do
+ ActiveSupport::Cache.lookup_store :mem_cache_store, memcache
+ end
+ end
+ end
+
+ def test_mem_cache_fragment_cache_store_with_multiple_servers
+ assert_called_with(Dalli::Client, :new, [%w[localhost 192.168.1.1], {}]) do
+ store = ActiveSupport::Cache.lookup_store :mem_cache_store, "localhost", "192.168.1.1"
+ assert_kind_of(ActiveSupport::Cache::MemCacheStore, store)
+ end
+ end
+
+ def test_mem_cache_fragment_cache_store_with_options
+ assert_called_with(Dalli::Client, :new, [%w[localhost 192.168.1.1], { timeout: 10 }]) do
+ 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
+ end
+
+ def test_object_assigned_fragment_cache_store
+ store = ActiveSupport::Cache.lookup_store ActiveSupport::Cache::FileStore.new("/path/to/cache/directory")
+ assert_kind_of(ActiveSupport::Cache::FileStore, store)
+ assert_equal "/path/to/cache/directory", store.cache_path
+ end
+end
diff --git a/activesupport/test/cache/cache_store_write_multi_test.rb b/activesupport/test/cache/cache_store_write_multi_test.rb
new file mode 100644
index 0000000000..16e3f3b842
--- /dev/null
+++ b/activesupport/test/cache/cache_store_write_multi_test.rb
@@ -0,0 +1,60 @@
+require "abstract_unit"
+require "active_support/cache"
+
+class CacheStoreWriteMultiEntriesStoreProviderInterfaceTest < ActiveSupport::TestCase
+ setup do
+ @cache = ActiveSupport::Cache.lookup_store(:null_store)
+ end
+
+ test "fetch_multi uses write_multi_entries store provider interface" do
+ assert_called_with(@cache, :write_multi_entries) do
+ @cache.fetch_multi "a", "b", "c" do |key|
+ key * 2
+ end
+ end
+ end
+end
+
+class CacheStoreWriteMultiInstrumentationTest < ActiveSupport::TestCase
+ setup do
+ @cache = ActiveSupport::Cache.lookup_store(:null_store)
+ end
+
+ test "instrumentation" do
+ writes = { "a" => "aa", "b" => "bb" }
+
+ events = with_instrumentation "write_multi" do
+ @cache.write_multi(writes)
+ end
+
+ assert_equal %w[ cache_write_multi.active_support ], events.map(&:name)
+ assert_nil events[0].payload[:super_operation]
+ assert_equal({ "a" => "aa", "b" => "bb" }, events[0].payload[:key])
+ end
+
+ test "instrumentation with fetch_multi as super operation" do
+ skip "fetch_multi isn't instrumented yet"
+
+ events = with_instrumentation "write_multi" do
+ @cache.fetch_multi("a", "b") { |key| key * 2 }
+ end
+
+ assert_equal %w[ cache_write_multi.active_support ], events.map(&:name)
+ assert_nil events[0].payload[:super_operation]
+ assert !events[0].payload[:hit]
+ end
+
+ private
+ def with_instrumentation(method)
+ event_name = "cache_#{method}.active_support"
+
+ [].tap do |events|
+ ActiveSupport::Notifications.subscribe event_name do |*args|
+ events << ActiveSupport::Notifications::Event.new(*args)
+ end
+ yield
+ end
+ ensure
+ ActiveSupport::Notifications.unsubscribe event_name
+ end
+end
diff --git a/activesupport/test/cache/local_cache_middleware_test.rb b/activesupport/test/cache/local_cache_middleware_test.rb
new file mode 100644
index 0000000000..352502fb43
--- /dev/null
+++ b/activesupport/test/cache/local_cache_middleware_test.rb
@@ -0,0 +1,61 @@
+require "abstract_unit"
+require "active_support/cache"
+
+module ActiveSupport
+ module Cache
+ module Strategy
+ module LocalCache
+ class MiddlewareTest < ActiveSupport::TestCase
+ def test_local_cache_cleared_on_close
+ key = "super awesome key"
+ assert_nil LocalCacheRegistry.cache_for key
+ middleware = Middleware.new("<3", key).new(->(env) {
+ assert LocalCacheRegistry.cache_for(key), "should have a cache"
+ [200, {}, []]
+ })
+ _, _, body = middleware.call({})
+ assert LocalCacheRegistry.cache_for(key), "should still have a cache"
+ body.each {}
+ assert LocalCacheRegistry.cache_for(key), "should still have a cache"
+ body.close
+ assert_nil LocalCacheRegistry.cache_for(key)
+ end
+
+ def test_local_cache_cleared_and_response_should_be_present_on_invalid_parameters_error
+ key = "super awesome key"
+ assert_nil LocalCacheRegistry.cache_for key
+ middleware = Middleware.new("<3", key).new(->(env) {
+ assert LocalCacheRegistry.cache_for(key), "should have a cache"
+ raise Rack::Utils::InvalidParameterError
+ })
+ response = middleware.call({})
+ assert response, "response should exist"
+ assert_nil LocalCacheRegistry.cache_for(key)
+ end
+
+ def test_local_cache_cleared_on_exception
+ key = "super awesome key"
+ assert_nil LocalCacheRegistry.cache_for key
+ middleware = Middleware.new("<3", key).new(->(env) {
+ assert LocalCacheRegistry.cache_for(key), "should have a cache"
+ raise
+ })
+ assert_raises(RuntimeError) { middleware.call({}) }
+ assert_nil LocalCacheRegistry.cache_for(key)
+ end
+
+ def test_local_cache_cleared_on_throw
+ key = "super awesome key"
+ assert_nil LocalCacheRegistry.cache_for key
+ middleware = Middleware.new("<3", key).new(->(env) {
+ assert LocalCacheRegistry.cache_for(key), "should have a cache"
+ throw :warden
+ })
+ assert_throws(:warden) { middleware.call({}) }
+ assert_nil LocalCacheRegistry.cache_for(key)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activesupport/test/cache/stores/file_store_test.rb b/activesupport/test/cache/stores/file_store_test.rb
new file mode 100644
index 0000000000..48b304fe6e
--- /dev/null
+++ b/activesupport/test/cache/stores/file_store_test.rb
@@ -0,0 +1,128 @@
+require "abstract_unit"
+require "active_support/cache"
+require_relative "../behaviors"
+require "pathname"
+
+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)
+ @cache_with_pathname = ActiveSupport::Cache.lookup_store(:file_store, Pathname.new(cache_dir), expires_in: 60)
+
+ @buffer = StringIO.new
+ @cache.logger = ActiveSupport::Logger.new(@buffer)
+ end
+
+ def teardown
+ FileUtils.rm_r(cache_dir)
+ rescue Errno::ENOENT
+ end
+
+ def cache_dir
+ File.join(Dir.pwd, "tmp_cache")
+ end
+
+ include CacheStoreBehavior
+ include CacheStoreVersionBehavior
+ include LocalCacheBehavior
+ include CacheDeleteMatchedBehavior
+ include CacheIncrementDecrementBehavior
+ include AutoloadingCacheBehavior
+
+ def test_clear
+ gitkeep = File.join(cache_dir, ".gitkeep")
+ keep = File.join(cache_dir, ".keep")
+ FileUtils.touch([gitkeep, keep])
+ @cache.clear
+ assert File.exist?(gitkeep)
+ assert File.exist?(keep)
+ end
+
+ def test_clear_without_cache_dir
+ FileUtils.rm_r(cache_dir)
+ @cache.clear
+ end
+
+ def test_long_uri_encoded_keys
+ @cache.write("%" * 870, 1)
+ assert_equal 1, @cache.read("%" * 870)
+ end
+
+ def test_key_transformation
+ key = @cache.send(:normalize_key, "views/index?id=1", {})
+ assert_equal "views/index?id=1", @cache.send(:file_path_key, key)
+ end
+
+ def test_key_transformation_with_pathname
+ FileUtils.touch(File.join(cache_dir, "foo"))
+ key = @cache_with_pathname.send(:normalize_key, "views/index?id=1", {})
+ assert_equal "views/index?id=1", @cache_with_pathname.send(:file_path_key, key)
+ end
+
+ # Test that generated cache keys are short enough to have Tempfile stuff added to them and
+ # remain valid
+ def test_filename_max_size
+ key = "#{'A' * ActiveSupport::Cache::FileStore::FILENAME_MAX_SIZE}"
+ path = @cache.send(:normalize_key, key, {})
+ Dir::Tmpname.create(path) do |tmpname, n, opts|
+ assert File.basename(tmpname + ".lock").length <= 255, "Temp filename too long: #{File.basename(tmpname + '.lock').length}"
+ end
+ end
+
+ # Because file systems have a maximum filename size, filenames > max size should be split in to directories
+ # If filename is 'AAAAB', where max size is 4, the returned path should be AAAA/B
+ def test_key_transformation_max_filename_size
+ key = "#{'A' * ActiveSupport::Cache::FileStore::FILENAME_MAX_SIZE}B"
+ path = @cache.send(:normalize_key, key, {})
+ assert path.split("/").all? { |dir_name| dir_name.size <= ActiveSupport::Cache::FileStore::FILENAME_MAX_SIZE }
+ assert_equal "B", File.basename(path)
+ end
+
+ # If nothing has been stored in the cache, there is a chance the cache directory does not yet exist
+ # Ensure delete_matched gracefully handles this case
+ def test_delete_matched_when_cache_directory_does_not_exist
+ assert_nothing_raised do
+ ActiveSupport::Cache::FileStore.new("/test/cache/directory").delete_matched(/does_not_exist/)
+ end
+ end
+
+ def test_delete_does_not_delete_empty_parent_dir
+ sub_cache_dir = File.join(cache_dir, "subdir/")
+ sub_cache_store = ActiveSupport::Cache::FileStore.new(sub_cache_dir)
+ assert_nothing_raised do
+ assert sub_cache_store.write("foo", "bar")
+ assert sub_cache_store.delete("foo")
+ end
+ assert File.exist?(cache_dir), "Parent of top level cache dir was deleted!"
+ assert File.exist?(sub_cache_dir), "Top level cache dir was deleted!"
+ assert Dir.entries(sub_cache_dir).reject { |f| ActiveSupport::Cache::FileStore::EXCLUDED_DIRS.include?(f) }.empty?
+ end
+
+ def test_log_exception_when_cache_read_fails
+ File.stub(:exist?, -> { raise StandardError.new("failed") }) do
+ @cache.send(:read_entry, "winston", {})
+ assert @buffer.string.present?
+ end
+ end
+
+ def test_cleanup_removes_all_expired_entries
+ time = Time.now
+ @cache.write("foo", "bar", expires_in: 10)
+ @cache.write("baz", "qux")
+ @cache.write("quux", "corge", expires_in: 20)
+ Time.stub(:now, time + 15) do
+ @cache.cleanup
+ assert_not @cache.exist?("foo")
+ assert @cache.exist?("baz")
+ assert @cache.exist?("quux")
+ end
+ end
+
+ def test_write_with_unless_exist
+ assert_equal true, @cache.write(1, "aaaaaaaaaa")
+ assert_equal false, @cache.write(1, "aaaaaaaaaa", unless_exist: true)
+ @cache.write(1, nil)
+ assert_equal false, @cache.write(1, "aaaaaaaaaa", unless_exist: true)
+ end
+end
diff --git a/activesupport/test/cache/stores/mem_cache_store_test.rb b/activesupport/test/cache/stores/mem_cache_store_test.rb
new file mode 100644
index 0000000000..2dd5264818
--- /dev/null
+++ b/activesupport/test/cache/stores/mem_cache_store_test.rb
@@ -0,0 +1,74 @@
+require "abstract_unit"
+require "active_support/cache"
+require_relative "../behaviors"
+require "dalli"
+
+class MemCacheStoreTest < ActiveSupport::TestCase
+ begin
+ ss = Dalli::Client.new("localhost:11211").stats
+ raise Dalli::DalliError unless ss["localhost:11211"]
+
+ MEMCACHE_UP = true
+ rescue Dalli::DalliError
+ $stderr.puts "Skipping memcached tests. Start memcached and try again."
+ MEMCACHE_UP = false
+ end
+
+ def setup
+ skip "memcache server is not up" unless MEMCACHE_UP
+
+ @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 = ActiveSupport::Logger.new("/dev/null")
+ end
+
+ include CacheStoreBehavior
+ include CacheStoreVersionBehavior
+ include LocalCacheBehavior
+ include CacheIncrementDecrementBehavior
+ include EncodedKeyCacheBehavior
+ include AutoloadingCacheBehavior
+
+ 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_raw_values_with_marshal
+ cache = ActiveSupport::Cache.lookup_store(:mem_cache_store, raw: true)
+ cache.clear
+ cache.write("foo", Marshal.dump([]))
+ assert_equal [], cache.read("foo")
+ end
+
+ 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
+
+ def test_local_cache_raw_values_with_marshal
+ cache = ActiveSupport::Cache.lookup_store(:mem_cache_store, raw: true)
+ cache.clear
+ cache.with_local_cache do
+ cache.write("foo", Marshal.dump([]))
+ assert_equal [], cache.read("foo")
+ end
+ end
+
+ def test_read_should_return_a_different_object_id_each_time_it_is_called
+ @cache.write("foo", "bar")
+ value = @cache.read("foo")
+ assert_not_equal value.object_id, @cache.read("foo").object_id
+ value << "bingo"
+ assert_not_equal value, @cache.read("foo")
+ end
+end
diff --git a/activesupport/test/cache/stores/memory_store_test.rb b/activesupport/test/cache/stores/memory_store_test.rb
new file mode 100644
index 0000000000..3dd1646d56
--- /dev/null
+++ b/activesupport/test/cache/stores/memory_store_test.rb
@@ -0,0 +1,107 @@
+require "abstract_unit"
+require "active_support/cache"
+require_relative "../behaviors"
+
+class MemoryStoreTest < ActiveSupport::TestCase
+ def setup
+ @record_size = ActiveSupport::Cache.lookup_store(:memory_store).send(:cached_size, 1, ActiveSupport::Cache::Entry.new("aaaaaaaaaa"))
+ @cache = ActiveSupport::Cache.lookup_store(:memory_store, expires_in: 60, size: @record_size * 10 + 1)
+ end
+
+ include CacheStoreBehavior
+ include CacheStoreVersionBehavior
+ 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(@record_size * 3)
+ assert @cache.exist?(5)
+ assert @cache.exist?(4)
+ assert !@cache.exist?(3), "no entry"
+ assert @cache.exist?(2)
+ assert !@cache.exist?(1), "no entry"
+ 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 @cache.exist?(11)
+ assert @cache.exist?(10)
+ assert @cache.exist?(9)
+ assert @cache.exist?(8)
+ assert @cache.exist?(7)
+ assert !@cache.exist?(6), "no entry"
+ assert !@cache.exist?(5), "no entry"
+ assert @cache.exist?(4)
+ assert !@cache.exist?(3), "no entry"
+ assert @cache.exist?(2)
+ assert !@cache.exist?(1), "no entry"
+ end
+
+ def test_prune_size_on_write_based_on_key_length
+ @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)
+ long_key = "*" * 2 * @record_size
+ @cache.write(long_key, "llllllllll")
+ assert @cache.exist?(long_key)
+ assert @cache.exist?(9)
+ assert @cache.exist?(8)
+ assert @cache.exist?(7)
+ assert @cache.exist?(6)
+ assert !@cache.exist?(5), "no entry"
+ assert !@cache.exist?(4), "no entry"
+ assert !@cache.exist?(3), "no entry"
+ assert !@cache.exist?(2), "no entry"
+ assert !@cache.exist?(1), "no entry"
+ 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 @cache.exist?(5)
+ assert @cache.exist?(4)
+ assert @cache.exist?(3)
+ assert @cache.exist?(2)
+ assert !@cache.exist?(1)
+ end
+
+ def test_write_with_unless_exist
+ assert_equal true, @cache.write(1, "aaaaaaaaaa")
+ assert_equal false, @cache.write(1, "aaaaaaaaaa", unless_exist: true)
+ @cache.write(1, nil)
+ assert_equal false, @cache.write(1, "aaaaaaaaaa", unless_exist: true)
+ end
+end
diff --git a/activesupport/test/cache/stores/null_store_test.rb b/activesupport/test/cache/stores/null_store_test.rb
new file mode 100644
index 0000000000..23c4e64ee4
--- /dev/null
+++ b/activesupport/test/cache/stores/null_store_test.rb
@@ -0,0 +1,57 @@
+require "abstract_unit"
+require "active_support/cache"
+require_relative "../behaviors"
+
+class NullStoreTest < ActiveSupport::TestCase
+ def setup
+ @cache = ActiveSupport::Cache.lookup_store(:null_store)
+ end
+
+ def test_clear
+ @cache.clear
+ end
+
+ def test_cleanup
+ @cache.cleanup
+ end
+
+ def test_write
+ assert_equal true, @cache.write("name", "value")
+ end
+
+ def test_read
+ @cache.write("name", "value")
+ assert_nil @cache.read("name")
+ end
+
+ def test_delete
+ @cache.write("name", "value")
+ assert_equal false, @cache.delete("name")
+ end
+
+ def test_increment
+ @cache.write("name", 1, raw: true)
+ assert_nil @cache.increment("name")
+ end
+
+ def test_decrement
+ @cache.write("name", 1, raw: true)
+ assert_nil @cache.increment("name")
+ end
+
+ def test_delete_matched
+ @cache.write("name", "value")
+ @cache.delete_matched(/name/)
+ end
+
+ def test_local_store_strategy
+ @cache.with_local_cache do
+ @cache.write("name", "value")
+ assert_equal "value", @cache.read("name")
+ @cache.delete("name")
+ assert_nil @cache.read("name")
+ @cache.write("name", "value")
+ end
+ assert_nil @cache.read("name")
+ end
+end
diff --git a/activesupport/test/caching_test.rb b/activesupport/test/caching_test.rb
deleted file mode 100644
index c67ffe69b8..0000000000
--- a/activesupport/test/caching_test.rb
+++ /dev/null
@@ -1,1203 +0,0 @@
-require "logger"
-require "abstract_unit"
-require "active_support/cache"
-require "dependencies_test_helpers"
-
-require "pathname"
-
-module ActiveSupport
- module Cache
- module Strategy
- module LocalCache
- class MiddlewareTest < ActiveSupport::TestCase
- def test_local_cache_cleared_on_close
- key = "super awesome key"
- assert_nil LocalCacheRegistry.cache_for key
- middleware = Middleware.new("<3", key).new(->(env) {
- assert LocalCacheRegistry.cache_for(key), "should have a cache"
- [200, {}, []]
- })
- _, _, body = middleware.call({})
- assert LocalCacheRegistry.cache_for(key), "should still have a cache"
- body.each {}
- assert LocalCacheRegistry.cache_for(key), "should still have a cache"
- body.close
- assert_nil LocalCacheRegistry.cache_for(key)
- end
-
- def test_local_cache_cleared_and_response_should_be_present_on_invalid_parameters_error
- key = "super awesome key"
- assert_nil LocalCacheRegistry.cache_for key
- middleware = Middleware.new("<3", key).new(->(env) {
- assert LocalCacheRegistry.cache_for(key), "should have a cache"
- raise Rack::Utils::InvalidParameterError
- })
- response = middleware.call({})
- assert response, "response should exist"
- assert_nil LocalCacheRegistry.cache_for(key)
- end
-
- def test_local_cache_cleared_on_exception
- key = "super awesome key"
- assert_nil LocalCacheRegistry.cache_for key
- middleware = Middleware.new("<3", key).new(->(env) {
- assert LocalCacheRegistry.cache_for(key), "should have a cache"
- raise
- })
- assert_raises(RuntimeError) { middleware.call({}) }
- assert_nil LocalCacheRegistry.cache_for(key)
- end
-
- def test_local_cache_cleared_on_throw
- key = "super awesome key"
- assert_nil LocalCacheRegistry.cache_for key
- middleware = Middleware.new("<3", key).new(->(env) {
- assert LocalCacheRegistry.cache_for(key), "should have a cache"
- throw :warden
- })
- assert_throws(:warden) { middleware.call({}) }
- assert_nil LocalCacheRegistry.cache_for(key)
- end
- end
- end
- end
- end
-end
-
-class CacheKeyTest < ActiveSupport::TestCase
- def test_entry_legacy_optional_ivars
- legacy = Class.new(ActiveSupport::Cache::Entry) do
- def initialize(value, options = {})
- @value = value
- @expires_in = nil
- @created_at = nil
- super
- end
- end
-
- entry = legacy.new "foo"
- assert_equal "foo", entry.value
- end
-
- 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
-
- def test_expand_cache_key_with_rails_cache_id
- with_env("RAILS_CACHE_ID" => "c99") do
- assert_equal "c99/foo", ActiveSupport::Cache.expand_cache_key(:foo)
- assert_equal "c99/foo", ActiveSupport::Cache.expand_cache_key([:foo])
- assert_equal "c99/foo/bar", ActiveSupport::Cache.expand_cache_key([:foo, :bar])
- assert_equal "nm/c99/foo", ActiveSupport::Cache.expand_cache_key(:foo, :nm)
- assert_equal "nm/c99/foo", ActiveSupport::Cache.expand_cache_key([:foo], :nm)
- assert_equal "nm/c99/foo/bar", ActiveSupport::Cache.expand_cache_key([:foo, :bar], :nm)
- end
- end
-
- def test_expand_cache_key_with_rails_app_version
- with_env("RAILS_APP_VERSION" => "rails3") do
- assert_equal "rails3/foo", ActiveSupport::Cache.expand_cache_key(:foo)
- end
- end
-
- def test_expand_cache_key_rails_cache_id_should_win_over_rails_app_version
- with_env("RAILS_CACHE_ID" => "c99", "RAILS_APP_VERSION" => "rails3") do
- assert_equal "c99/foo", ActiveSupport::Cache.expand_cache_key(:foo)
- end
- end
-
- def test_expand_cache_key_respond_to_cache_key
- key = "foo"
- def key.cache_key
- :foo_key
- end
- assert_equal "foo_key", ActiveSupport::Cache.expand_cache_key(key)
- end
-
- def test_expand_cache_key_array_with_something_that_responds_to_cache_key
- key = "foo"
- def key.cache_key
- :foo_key
- end
- assert_equal "foo_key", ActiveSupport::Cache.expand_cache_key([key])
- end
-
- def test_expand_cache_key_of_nil
- assert_equal "", ActiveSupport::Cache.expand_cache_key(nil)
- end
-
- def test_expand_cache_key_of_false
- assert_equal "false", ActiveSupport::Cache.expand_cache_key(false)
- end
-
- def test_expand_cache_key_of_true
- assert_equal "true", ActiveSupport::Cache.expand_cache_key(true)
- end
-
- def test_expand_cache_key_of_array_like_object
- assert_equal "foo/bar/baz", ActiveSupport::Cache.expand_cache_key(%w{foo bar baz}.to_enum)
- end
-
- private
-
- def with_env(kv)
- old_values = {}
- kv.each { |key, value| old_values[key], ENV[key] = ENV[key], value }
- yield
- ensure
- old_values.each { |key, value| ENV[key] = value }
- end
-end
-
-class CacheStoreSettingTest < ActiveSupport::TestCase
- def test_memory_store_gets_created_if_no_arguments_passed_to_lookup_store_method
- store = ActiveSupport::Cache.lookup_store
- assert_kind_of(ActiveSupport::Cache::MemoryStore, store)
- end
-
- def test_memory_store
- store = ActiveSupport::Cache.lookup_store :memory_store
- assert_kind_of(ActiveSupport::Cache::MemoryStore, store)
- end
-
- def test_file_fragment_cache_store
- store = ActiveSupport::Cache.lookup_store :file_store, "/path/to/cache/directory"
- assert_kind_of(ActiveSupport::Cache::FileStore, store)
- assert_equal "/path/to/cache/directory", store.cache_path
- end
-
- def test_mem_cache_fragment_cache_store
- assert_called_with(Dalli::Client, :new, [%w[localhost], {}]) do
- store = ActiveSupport::Cache.lookup_store :mem_cache_store, "localhost"
- assert_kind_of(ActiveSupport::Cache::MemCacheStore, store)
- end
- end
-
- def test_mem_cache_fragment_cache_store_with_given_mem_cache
- mem_cache = Dalli::Client.new
- assert_not_called(Dalli::Client, :new) do
- store = ActiveSupport::Cache.lookup_store :mem_cache_store, mem_cache
- assert_kind_of(ActiveSupport::Cache::MemCacheStore, store)
- end
- end
-
- def test_mem_cache_fragment_cache_store_with_not_dalli_client
- assert_not_called(Dalli::Client, :new) do
- memcache = Object.new
- assert_raises(ArgumentError) do
- ActiveSupport::Cache.lookup_store :mem_cache_store, memcache
- end
- end
- end
-
- def test_mem_cache_fragment_cache_store_with_multiple_servers
- assert_called_with(Dalli::Client, :new, [%w[localhost 192.168.1.1], {}]) do
- store = ActiveSupport::Cache.lookup_store :mem_cache_store, "localhost", "192.168.1.1"
- assert_kind_of(ActiveSupport::Cache::MemCacheStore, store)
- end
- end
-
- def test_mem_cache_fragment_cache_store_with_options
- assert_called_with(Dalli::Client, :new, [%w[localhost 192.168.1.1], { timeout: 10 }]) do
- 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
- end
-
- def test_object_assigned_fragment_cache_store
- store = ActiveSupport::Cache.lookup_store ActiveSupport::Cache::FileStore.new("/path/to/cache/directory")
- assert_kind_of(ActiveSupport::Cache::FileStore, store)
- assert_equal "/path/to/cache/directory", store.cache_path
- end
-end
-
-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 !cache.exist?("foo")
- assert 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 !cache.exist?("foo")
- assert 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 @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.write("foo", "bar")
- assert_not_called(@cache, :write) do
- assert_equal "bar", @cache.fetch("foo") { "baz" }
- end
- end
-
- def test_fetch_with_cache_miss
- assert_called_with(@cache, :write, ["foo", "baz", @cache.options]) do
- assert_equal "baz", @cache.fetch("foo") { "baz" }
- end
- end
-
- def test_fetch_with_cache_miss_passes_key_to_block
- cache_miss = false
- assert_equal 3, @cache.fetch("foo") { |key| cache_miss = true; key.length }
- assert cache_miss
-
- cache_miss = false
- assert_equal 3, @cache.fetch("foo") { |key| cache_miss = true; key.length }
- assert !cache_miss
- end
-
- def test_fetch_with_forced_cache_miss
- @cache.write("foo", "bar")
- assert_not_called(@cache, :read) do
- assert_called_with(@cache, :write, ["foo", "bar", @cache.options.merge(force: true)]) do
- @cache.fetch("foo", force: true) { "bar" }
- end
- end
- end
-
- def test_fetch_with_cached_nil
- @cache.write("foo", nil)
- assert_not_called(@cache, :write) do
- assert_nil @cache.fetch("foo") { "baz" }
- end
- end
-
- def test_fetch_with_forced_cache_miss_with_block
- @cache.write("foo", "bar")
- assert_equal "foo_bar", @cache.fetch("foo", force: true) { "foo_bar" }
- end
-
- def test_fetch_with_forced_cache_miss_without_block
- @cache.write("foo", "bar")
- assert_raises(ArgumentError) do
- @cache.fetch("foo", force: true)
- end
-
- assert_equal "bar", @cache.read("foo")
- end
-
- def test_should_read_and_write_hash
- assert @cache.write("foo", a: "b")
- assert_equal({ a: "b" }, @cache.read("foo"))
- end
-
- def test_should_read_and_write_integer
- assert @cache.write("foo", 1)
- assert_equal 1, @cache.read("foo")
- end
-
- def test_should_read_and_write_nil
- assert @cache.write("foo", nil)
- assert_nil @cache.read("foo")
- end
-
- def test_should_read_and_write_false
- assert @cache.write("foo", false)
- assert_equal false, @cache.read("foo")
- end
-
- def test_read_multi
- @cache.write("foo", "bar")
- @cache.write("fu", "baz")
- @cache.write("fud", "biz")
- assert_equal({ "foo" => "bar", "fu" => "baz" }, @cache.read_multi("foo", "fu"))
- end
-
- def test_read_multi_with_expires
- time = Time.now
- @cache.write("foo", "bar", expires_in: 10)
- @cache.write("fu", "baz")
- @cache.write("fud", "biz")
- Time.stub(:now, time + 11) do
- assert_equal({ "fu" => "baz" }, @cache.read_multi("foo", "fu"))
- end
- end
-
- def test_fetch_multi
- @cache.write("foo", "bar")
- @cache.write("fud", "biz")
-
- values = @cache.fetch_multi("foo", "fu", "fud") { |value| value * 2 }
-
- assert_equal({ "foo" => "bar", "fu" => "fufu", "fud" => "biz" }, values)
- assert_equal("fufu", @cache.read("fu"))
- end
-
- def test_multi_with_objects
- cache_struct = Struct.new(:cache_key, :title)
- foo = cache_struct.new("foo", "FOO!")
- bar = cache_struct.new("bar")
-
- @cache.write("bar", "BAM!")
-
- values = @cache.fetch_multi(foo, bar) { |object| object.title }
-
- assert_equal({ foo => "FOO!", bar => "BAM!" }, values)
- end
-
- def test_fetch_multi_without_block
- assert_raises(ArgumentError) do
- @cache.fetch_multi("foo")
- end
- end
-
- def test_read_and_write_compressed_small_data
- @cache.write("foo", "bar", compress: true)
- assert_equal "bar", @cache.read("foo")
- end
-
- def test_read_and_write_compressed_large_data
- @cache.write("foo", "bar", compress: true, compress_threshold: 2)
- assert_equal "bar", @cache.read("foo")
- end
-
- def test_read_and_write_compressed_nil
- @cache.write("foo", nil, compress: true)
- assert_nil @cache.read("foo")
- end
-
- 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_param_as_cache_key
- obj = Object.new
- def obj.to_param
- "foo"
- end
- @cache.write(obj, "bar")
- assert_equal "bar", @cache.read("foo")
- end
-
- def test_array_as_cache_key
- @cache.write([:fu, "foo"], "bar")
- assert_equal "bar", @cache.read("fu/foo")
- end
-
- def test_hash_as_cache_key
- @cache.write({ foo: 1, fu: 2 }, "bar")
- assert_equal "bar", @cache.read("foo=1/fu=2")
- end
-
- def test_keys_are_case_sensitive
- @cache.write("foo", "bar")
- assert_nil @cache.read("FOO")
- end
-
- def test_exist
- @cache.write("foo", "bar")
- assert_equal true, @cache.exist?("foo")
- assert_equal false, @cache.exist?("bar")
- end
-
- def test_nil_exist
- @cache.write("foo", nil)
- assert @cache.exist?("foo")
- end
-
- def test_delete
- @cache.write("foo", "bar")
- assert @cache.exist?("foo")
- assert @cache.delete("foo")
- assert !@cache.exist?("foo")
- end
-
- def test_original_store_objects_should_not_be_immutable
- bar = "bar"
- @cache.write("foo", bar)
- assert_nothing_raised { bar.gsub!(/.*/, "baz") }
- end
-
- def test_expires_in
- time = Time.local(2008, 4, 24)
-
- Time.stub(:now, time) do
- @cache.write("foo", "bar")
- assert_equal "bar", @cache.read("foo")
- end
-
- Time.stub(:now, time + 30) do
- assert_equal "bar", @cache.read("foo")
- end
-
- Time.stub(:now, time + 61) do
- assert_nil @cache.read("foo")
- end
- end
-
- def test_race_condition_protection_skipped_if_not_defined
- @cache.write("foo", "bar")
- time = @cache.send(:read_entry, @cache.send(:normalize_key, "foo", {}), {}).expires_at
-
- Time.stub(:now, Time.at(time)) do
- result = @cache.fetch("foo") do
- assert_nil @cache.read("foo")
- "baz"
- end
- assert_equal "baz", result
- end
- end
-
- def test_race_condition_protection_is_limited
- time = Time.now
- @cache.write("foo", "bar", expires_in: 60)
- Time.stub(:now, time + 71) do
- result = @cache.fetch("foo", race_condition_ttl: 10) do
- assert_nil @cache.read("foo")
- "baz"
- end
- assert_equal "baz", result
- end
- end
-
- def test_race_condition_protection_is_safe
- time = Time.now
- @cache.write("foo", "bar", expires_in: 60)
- Time.stub(:now, time + 61) do
- begin
- @cache.fetch("foo", race_condition_ttl: 10) do
- assert_equal "bar", @cache.read("foo")
- raise ArgumentError.new
- end
- rescue ArgumentError
- end
- assert_equal "bar", @cache.read("foo")
- end
- Time.stub(:now, time + 91) do
- assert_nil @cache.read("foo")
- end
- end
-
- def test_race_condition_protection
- time = Time.now
- @cache.write("foo", "bar", expires_in: 60)
- Time.stub(:now, time + 61) do
- result = @cache.fetch("foo", race_condition_ttl: 10) do
- assert_equal "bar", @cache.read("foo")
- "baz"
- end
- assert_equal "baz", result
- end
- end
-
- def test_crazy_key_characters
- crazy_key = "#/:*(<+=> )&$%@?;'\"\'`~-"
- assert @cache.write(crazy_key, "1", raw: true)
- assert_equal "1", @cache.read(crazy_key)
- assert_equal "1", @cache.fetch(crazy_key)
- assert @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 = ""
- 900.times { key << "x" }
- assert @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 @cache.delete(key)
- end
-
- def test_cache_hit_instrumentation
- key = "test_key"
- @events = []
- ActiveSupport::Notifications.subscribe "cache_read.active_support" do |*args|
- @events << ActiveSupport::Notifications::Event.new(*args)
- end
- assert @cache.write(key, "1", raw: true)
- assert @cache.fetch(key) {}
- assert_equal 1, @events.length
- assert_equal "cache_read.active_support", @events[0].name
- assert_equal :fetch, @events[0].payload[:super_operation]
- assert @events[0].payload[:hit]
- ensure
- ActiveSupport::Notifications.unsubscribe "cache_read.active_support"
- end
-
- def test_cache_miss_instrumentation
- @events = []
- ActiveSupport::Notifications.subscribe(/^cache_(.*)\.active_support$/) do |*args|
- @events << ActiveSupport::Notifications::Event.new(*args)
- end
- assert_not @cache.fetch("bad_key") {}
- assert_equal 3, @events.length
- assert_equal "cache_read.active_support", @events[0].name
- assert_equal "cache_generate.active_support", @events[1].name
- assert_equal "cache_write.active_support", @events[2].name
- assert_equal :fetch, @events[0].payload[:super_operation]
- assert_not @events[0].payload[:hit]
- ensure
- ActiveSupport::Notifications.unsubscribe "cache_read.active_support"
- end
-end
-
-# https://rails.lighthouseapp.com/projects/8994/tickets/6225-memcachestore-cant-deal-with-umlauts-and-special-characters
-# The error is caused by character encodings that can't be compared with ASCII-8BIT regular expressions and by special
-# characters like the umlaut in UTF-8.
-module EncodedKeyCacheBehavior
- Encoding.list.each do |encoding|
- define_method "test_#{encoding.name.underscore}_encoded_values" do
- key = "foo".force_encoding(encoding)
- assert @cache.write(key, "1", raw: true)
- assert_equal "1", @cache.read(key)
- assert_equal "1", @cache.fetch(key)
- assert @cache.delete(key)
- assert_equal "2", @cache.fetch(key, raw: true) { "2" }
- assert_equal 3, @cache.increment(key)
- assert_equal 2, @cache.decrement(key)
- end
- end
-
- def test_common_utf8_values
- key = "\xC3\xBCmlaut".force_encoding(Encoding::UTF_8)
- assert @cache.write(key, "1", raw: true)
- assert_equal "1", @cache.read(key)
- assert_equal "1", @cache.fetch(key)
- assert @cache.delete(key)
- assert_equal "2", @cache.fetch(key, raw: true) { "2" }
- assert_equal 3, @cache.increment(key)
- assert_equal 2, @cache.decrement(key)
- end
-
- def test_retains_encoding
- key = "\xC3\xBCmlaut".force_encoding(Encoding::UTF_8)
- assert @cache.write(key, "1", raw: true)
- assert_equal Encoding::UTF_8, key.encoding
- end
-end
-
-module CacheDeleteMatchedBehavior
- def test_delete_matched
- @cache.write("foo", "bar")
- @cache.write("fu", "baz")
- @cache.write("foo/bar", "baz")
- @cache.write("fu/baz", "bar")
- @cache.delete_matched(/oo/)
- assert !@cache.exist?("foo")
- assert @cache.exist?("fu")
- assert !@cache.exist?("foo/bar")
- assert @cache.exist?("fu/baz")
- 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
- assert_nil @cache.increment("bar")
- 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
- assert_nil @cache.decrement("bar")
- 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 retval
- assert_equal "bar", @cache.read("foo")
- 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
-
- 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_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_nil
- @cache.with_local_cache do
- assert_nil @cache.read("foo")
- @cache.send(:bypass_local_cache) { @cache.write "foo", "bar" }
- assert_nil @cache.read("foo")
- end
- end
-
- def test_local_cache_fetch
- @cache.with_local_cache do
- @cache.send(:local_cache).write "foo", "bar"
- assert_equal "bar", @cache.send(:local_cache).fetch("foo")
- end
- end
-
- def test_local_cache_of_write_nil
- @cache.with_local_cache do
- assert @cache.write("foo", nil)
- assert_nil @cache.read("foo")
- @peek.write("foo", "bar")
- 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")
- assert_nil @cache.read("foo")
- end
- end
-
- def test_local_cache_of_exist
- @cache.with_local_cache do
- @cache.write("foo", "bar")
- @peek.delete("foo")
- assert @cache.exist?("foo")
- end
- 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_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_middleware
- app = lambda { |env|
- result = @cache.write("foo", "bar")
- assert_equal "bar", @cache.read("foo") # make sure 'foo' was written
- assert result
- [200, {}, []]
- }
- app = @cache.middleware.new(app)
- app.call({})
- end
-end
-
-module AutoloadingCacheBehavior
- include DependenciesTestHelpers
- def test_simple_autoloading
- with_autoloading_fixtures do
- @cache.write("foo", EM.new)
- end
-
- remove_constants(:EM)
- ActiveSupport::Dependencies.clear
-
- with_autoloading_fixtures do
- assert_kind_of EM, @cache.read("foo")
- end
-
- remove_constants(:EM)
- ActiveSupport::Dependencies.clear
- end
-
- def test_two_classes_autoloading
- with_autoloading_fixtures do
- @cache.write("foo", [EM.new, ClassFolder.new])
- end
-
- remove_constants(:EM, :ClassFolder)
- ActiveSupport::Dependencies.clear
-
- with_autoloading_fixtures do
- loaded = @cache.read("foo")
- assert_kind_of Array, loaded
- assert_equal 2, loaded.size
- assert_kind_of EM, loaded[0]
- assert_kind_of ClassFolder, loaded[1]
- end
-
- remove_constants(:EM, :ClassFolder)
- ActiveSupport::Dependencies.clear
- end
-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)
- @cache_with_pathname = ActiveSupport::Cache.lookup_store(:file_store, Pathname.new(cache_dir), expires_in: 60)
-
- @buffer = StringIO.new
- @cache.logger = ActiveSupport::Logger.new(@buffer)
- end
-
- def teardown
- FileUtils.rm_r(cache_dir)
- rescue Errno::ENOENT
- end
-
- def cache_dir
- File.join(Dir.pwd, "tmp_cache")
- end
-
- include CacheStoreBehavior
- include LocalCacheBehavior
- include CacheDeleteMatchedBehavior
- include CacheIncrementDecrementBehavior
- include AutoloadingCacheBehavior
-
- def test_clear
- gitkeep = File.join(cache_dir, ".gitkeep")
- keep = File.join(cache_dir, ".keep")
- FileUtils.touch([gitkeep, keep])
- @cache.clear
- assert File.exist?(gitkeep)
- assert File.exist?(keep)
- end
-
- def test_clear_without_cache_dir
- FileUtils.rm_r(cache_dir)
- @cache.clear
- end
-
- def test_long_uri_encoded_keys
- @cache.write("%" * 870, 1)
- assert_equal 1, @cache.read("%" * 870)
- end
-
- def test_key_transformation
- key = @cache.send(:normalize_key, "views/index?id=1", {})
- assert_equal "views/index?id=1", @cache.send(:file_path_key, key)
- end
-
- def test_key_transformation_with_pathname
- FileUtils.touch(File.join(cache_dir, "foo"))
- key = @cache_with_pathname.send(:normalize_key, "views/index?id=1", {})
- assert_equal "views/index?id=1", @cache_with_pathname.send(:file_path_key, key)
- end
-
- # Test that generated cache keys are short enough to have Tempfile stuff added to them and
- # remain valid
- def test_filename_max_size
- key = "#{'A' * ActiveSupport::Cache::FileStore::FILENAME_MAX_SIZE}"
- path = @cache.send(:normalize_key, key, {})
- Dir::Tmpname.create(path) do |tmpname, n, opts|
- assert File.basename(tmpname + ".lock").length <= 255, "Temp filename too long: #{File.basename(tmpname + '.lock').length}"
- end
- end
-
- # Because file systems have a maximum filename size, filenames > max size should be split in to directories
- # If filename is 'AAAAB', where max size is 4, the returned path should be AAAA/B
- def test_key_transformation_max_filename_size
- key = "#{'A' * ActiveSupport::Cache::FileStore::FILENAME_MAX_SIZE}B"
- path = @cache.send(:normalize_key, key, {})
- assert path.split("/").all? { |dir_name| dir_name.size <= ActiveSupport::Cache::FileStore::FILENAME_MAX_SIZE }
- assert_equal "B", File.basename(path)
- end
-
- # If nothing has been stored in the cache, there is a chance the cache directory does not yet exist
- # Ensure delete_matched gracefully handles this case
- def test_delete_matched_when_cache_directory_does_not_exist
- assert_nothing_raised do
- ActiveSupport::Cache::FileStore.new("/test/cache/directory").delete_matched(/does_not_exist/)
- end
- end
-
- def test_delete_does_not_delete_empty_parent_dir
- sub_cache_dir = File.join(cache_dir, "subdir/")
- sub_cache_store = ActiveSupport::Cache::FileStore.new(sub_cache_dir)
- assert_nothing_raised do
- assert sub_cache_store.write("foo", "bar")
- assert sub_cache_store.delete("foo")
- end
- assert File.exist?(cache_dir), "Parent of top level cache dir was deleted!"
- assert File.exist?(sub_cache_dir), "Top level cache dir was deleted!"
- assert Dir.entries(sub_cache_dir).reject { |f| ActiveSupport::Cache::FileStore::EXCLUDED_DIRS.include?(f) }.empty?
- end
-
- def test_log_exception_when_cache_read_fails
- File.stub(:exist?, -> { raise StandardError.new("failed") }) do
- @cache.send(:read_entry, "winston", {})
- assert @buffer.string.present?
- end
- end
-
- def test_cleanup_removes_all_expired_entries
- time = Time.now
- @cache.write("foo", "bar", expires_in: 10)
- @cache.write("baz", "qux")
- @cache.write("quux", "corge", expires_in: 20)
- Time.stub(:now, time + 15) do
- @cache.cleanup
- assert_not @cache.exist?("foo")
- assert @cache.exist?("baz")
- assert @cache.exist?("quux")
- end
- end
-
- def test_write_with_unless_exist
- assert_equal true, @cache.write(1, "aaaaaaaaaa")
- assert_equal false, @cache.write(1, "aaaaaaaaaa", unless_exist: true)
- @cache.write(1, nil)
- assert_equal false, @cache.write(1, "aaaaaaaaaa", unless_exist: true)
- end
-end
-
-class MemoryStoreTest < ActiveSupport::TestCase
- def setup
- @record_size = ActiveSupport::Cache.lookup_store(:memory_store).send(:cached_size, 1, ActiveSupport::Cache::Entry.new("aaaaaaaaaa"))
- @cache = ActiveSupport::Cache.lookup_store(:memory_store, expires_in: 60, size: @record_size * 10 + 1)
- 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(@record_size * 3)
- assert @cache.exist?(5)
- assert @cache.exist?(4)
- assert !@cache.exist?(3), "no entry"
- assert @cache.exist?(2)
- assert !@cache.exist?(1), "no entry"
- 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 @cache.exist?(11)
- assert @cache.exist?(10)
- assert @cache.exist?(9)
- assert @cache.exist?(8)
- assert @cache.exist?(7)
- assert !@cache.exist?(6), "no entry"
- assert !@cache.exist?(5), "no entry"
- assert @cache.exist?(4)
- assert !@cache.exist?(3), "no entry"
- assert @cache.exist?(2)
- assert !@cache.exist?(1), "no entry"
- end
-
- def test_prune_size_on_write_based_on_key_length
- @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)
- long_key = "*" * 2 * @record_size
- @cache.write(long_key, "llllllllll")
- assert @cache.exist?(long_key)
- assert @cache.exist?(9)
- assert @cache.exist?(8)
- assert @cache.exist?(7)
- assert @cache.exist?(6)
- assert !@cache.exist?(5), "no entry"
- assert !@cache.exist?(4), "no entry"
- assert !@cache.exist?(3), "no entry"
- assert !@cache.exist?(2), "no entry"
- assert !@cache.exist?(1), "no entry"
- 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 @cache.exist?(5)
- assert @cache.exist?(4)
- assert @cache.exist?(3)
- assert @cache.exist?(2)
- assert !@cache.exist?(1)
- end
-
- def test_write_with_unless_exist
- assert_equal true, @cache.write(1, "aaaaaaaaaa")
- assert_equal false, @cache.write(1, "aaaaaaaaaa", unless_exist: true)
- @cache.write(1, nil)
- assert_equal false, @cache.write(1, "aaaaaaaaaa", unless_exist: true)
- end
-end
-
-class MemCacheStoreTest < ActiveSupport::TestCase
- require "dalli"
-
- begin
- ss = Dalli::Client.new("localhost:11211").stats
- raise Dalli::DalliError unless ss["localhost:11211"]
-
- MEMCACHE_UP = true
- rescue Dalli::DalliError
- $stderr.puts "Skipping memcached tests. Start memcached and try again."
- MEMCACHE_UP = false
- end
-
- def setup
- skip "memcache server is not up" unless MEMCACHE_UP
-
- @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 = ActiveSupport::Logger.new("/dev/null")
- end
-
- include CacheStoreBehavior
- include LocalCacheBehavior
- include CacheIncrementDecrementBehavior
- include EncodedKeyCacheBehavior
- include AutoloadingCacheBehavior
-
- 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_raw_values_with_marshal
- cache = ActiveSupport::Cache.lookup_store(:mem_cache_store, raw: true)
- cache.clear
- cache.write("foo", Marshal.dump([]))
- assert_equal [], cache.read("foo")
- end
-
- 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
-
- def test_local_cache_raw_values_with_marshal
- cache = ActiveSupport::Cache.lookup_store(:mem_cache_store, raw: true)
- cache.clear
- cache.with_local_cache do
- cache.write("foo", Marshal.dump([]))
- assert_equal [], cache.read("foo")
- end
- end
-
- def test_read_should_return_a_different_object_id_each_time_it_is_called
- @cache.write("foo", "bar")
- value = @cache.read("foo")
- assert_not_equal value.object_id, @cache.read("foo").object_id
- value << "bingo"
- assert_not_equal value, @cache.read("foo")
- end
-end
-
-class NullStoreTest < ActiveSupport::TestCase
- def setup
- @cache = ActiveSupport::Cache.lookup_store(:null_store)
- end
-
- def test_clear
- @cache.clear
- end
-
- def test_cleanup
- @cache.cleanup
- end
-
- def test_write
- assert_equal true, @cache.write("name", "value")
- end
-
- def test_read
- @cache.write("name", "value")
- assert_nil @cache.read("name")
- end
-
- def test_delete
- @cache.write("name", "value")
- assert_equal false, @cache.delete("name")
- end
-
- def test_increment
- @cache.write("name", 1, raw: true)
- assert_nil @cache.increment("name")
- end
-
- def test_decrement
- @cache.write("name", 1, raw: true)
- assert_nil @cache.increment("name")
- end
-
- def test_delete_matched
- @cache.write("name", "value")
- @cache.delete_matched(/name/)
- end
-
- def test_local_store_strategy
- @cache.with_local_cache do
- @cache.write("name", "value")
- assert_equal "value", @cache.read("name")
- @cache.delete("name")
- assert_nil @cache.read("name")
- @cache.write("name", "value")
- end
- assert_nil @cache.read("name")
- end
-end
-
-class CacheStoreLoggerTest < ActiveSupport::TestCase
- def setup
- @cache = ActiveSupport::Cache.lookup_store(:memory_store)
-
- @buffer = StringIO.new
- @cache.logger = ActiveSupport::Logger.new(@buffer)
- end
-
- def test_logging
- @cache.fetch("foo") { "bar" }
- assert @buffer.string.present?
- end
-
- def test_log_with_string_namespace
- @cache.fetch("foo", namespace: "string_namespace") { "bar" }
- assert_match %r{string_namespace:foo}, @buffer.string
- end
-
- def test_log_with_proc_namespace
- proc = Proc.new do
- "proc_namespace"
- end
- @cache.fetch("foo", namespace: proc) { "bar" }
- assert_match %r{proc_namespace:foo}, @buffer.string
- end
-
- def test_mute_logging
- @cache.mute { @cache.fetch("foo") { "bar" } }
- assert @buffer.string.blank?
- end
-end
-
-class CacheEntryTest < ActiveSupport::TestCase
- def test_expired
- entry = ActiveSupport::Cache::Entry.new("value")
- assert !entry.expired?, "entry not expired"
- entry = ActiveSupport::Cache::Entry.new("value", expires_in: 60)
- assert !entry.expired?, "entry not expired"
- Time.stub(:now, Time.now + 61) do
- assert entry.expired?, "entry is expired"
- end
- end
-
- def test_compress_values
- value = "value" * 100
- entry = ActiveSupport::Cache::Entry.new(value, compress: true, compress_threshold: 1)
- assert_equal value, entry.value
- assert(value.bytesize > entry.size, "value is compressed")
- end
-
- def test_non_compress_values
- value = "value" * 100
- entry = ActiveSupport::Cache::Entry.new(value)
- assert_equal value, entry.value
- assert_equal value.bytesize, entry.size
- end
-end
diff --git a/activesupport/test/core_ext/class/attribute_test.rb b/activesupport/test/core_ext/class/attribute_test.rb
index 5a9ec78cc1..f16043c612 100644
--- a/activesupport/test/core_ext/class/attribute_test.rb
+++ b/activesupport/test/core_ext/class/attribute_test.rb
@@ -3,7 +3,11 @@ require "active_support/core_ext/class/attribute"
class ClassAttributeTest < ActiveSupport::TestCase
def setup
- @klass = Class.new { class_attribute :setting }
+ @klass = Class.new do
+ class_attribute :setting
+ class_attribute :timeout, default: 5
+ end
+
@sub = Class.new(@klass)
end
@@ -12,6 +16,10 @@ class ClassAttributeTest < ActiveSupport::TestCase
assert_nil @sub.setting
end
+ test "custom default" do
+ assert_equal 5, @klass.timeout
+ end
+
test "inheritable" do
@klass.setting = 1
assert_equal 1, @sub.setting
diff --git a/activesupport/test/core_ext/date_time_ext_test.rb b/activesupport/test/core_ext/date_time_ext_test.rb
index be7c14e9b4..276fa2bfd3 100644
--- a/activesupport/test/core_ext/date_time_ext_test.rb
+++ b/activesupport/test/core_ext/date_time_ext_test.rb
@@ -28,6 +28,28 @@ class DateTimeExtCalculationsTest < ActiveSupport::TestCase
end
end
+ def test_next_occur
+ datetime = DateTime.new(2016, 9, 24, 0, 0) # saturday
+ assert_equal datetime.next_occurring(:monday), datetime.since(2.days)
+ assert_equal datetime.next_occurring(:tuesday), datetime.since(3.days)
+ assert_equal datetime.next_occurring(:wednesday), datetime.since(4.days)
+ assert_equal datetime.next_occurring(:thursday), datetime.since(5.days)
+ assert_equal datetime.next_occurring(:friday), datetime.since(6.days)
+ assert_equal datetime.next_occurring(:saturday), datetime.since(1.week)
+ assert_equal datetime.next_occurring(:sunday), datetime.since(1.day)
+ end
+
+ def test_prev_occur
+ datetime = DateTime.new(2016, 9, 24, 0, 0) # saturday
+ assert_equal datetime.prev_occurring(:monday), datetime.ago(5.days)
+ assert_equal datetime.prev_occurring(:tuesday), datetime.ago(4.days)
+ assert_equal datetime.prev_occurring(:wednesday), datetime.ago(3.days)
+ assert_equal datetime.prev_occurring(:thursday), datetime.ago(2.days)
+ assert_equal datetime.prev_occurring(:friday), datetime.ago(1.day)
+ assert_equal datetime.prev_occurring(:saturday), datetime.ago(1.week)
+ assert_equal datetime.prev_occurring(:sunday), datetime.ago(6.days)
+ end
+
def test_readable_inspect
datetime = DateTime.new(2005, 2, 21, 14, 30, 0)
assert_equal "Mon, 21 Feb 2005 14:30:00 +0000", datetime.readable_inspect
diff --git a/activesupport/test/core_ext/duration_test.rb b/activesupport/test/core_ext/duration_test.rb
index 1648a9b270..cd1b505c34 100644
--- a/activesupport/test/core_ext/duration_test.rb
+++ b/activesupport/test/core_ext/duration_test.rb
@@ -315,7 +315,7 @@ class DurationTest < ActiveSupport::TestCase
assert_equal(1, scalar <=> 5)
assert_equal(0, scalar <=> 10)
assert_equal(-1, scalar <=> 15)
- assert_equal(nil, scalar <=> "foo")
+ assert_nil(scalar <=> "foo")
end
def test_scalar_plus
@@ -337,6 +337,13 @@ class DurationTest < ActiveSupport::TestCase
assert_equal "no implicit conversion of String into ActiveSupport::Duration::Scalar", exception.message
end
+ def test_scalar_plus_parts
+ scalar = ActiveSupport::Duration::Scalar.new(10)
+
+ assert_equal({ days: 1, seconds: 10 }, (scalar + 1.day).parts)
+ assert_equal({ days: -1, seconds: 10 }, (scalar + -1.day).parts)
+ end
+
def test_scalar_minus
scalar = ActiveSupport::Duration::Scalar.new(10)
@@ -349,6 +356,9 @@ class DurationTest < ActiveSupport::TestCase
assert_equal 5, scalar - 5.seconds
assert_instance_of ActiveSupport::Duration, scalar - 5.seconds
+ assert_equal({ days: -1, seconds: 10 }, (scalar - 1.day).parts)
+ assert_equal({ days: 1, seconds: 10 }, (scalar - -1.day).parts)
+
exception = assert_raises(TypeError) do
scalar - "foo"
end
@@ -356,6 +366,13 @@ class DurationTest < ActiveSupport::TestCase
assert_equal "no implicit conversion of String into ActiveSupport::Duration::Scalar", exception.message
end
+ def test_scalar_minus_parts
+ scalar = ActiveSupport::Duration::Scalar.new(10)
+
+ assert_equal({ days: -1, seconds: 10 }, (scalar - 1.day).parts)
+ assert_equal({ days: 1, seconds: 10 }, (scalar - -1.day).parts)
+ end
+
def test_scalar_multiply
scalar = ActiveSupport::Duration::Scalar.new(5)
@@ -375,6 +392,14 @@ class DurationTest < ActiveSupport::TestCase
assert_equal "no implicit conversion of String into ActiveSupport::Duration::Scalar", exception.message
end
+ def test_scalar_multiply_parts
+ scalar = ActiveSupport::Duration::Scalar.new(1)
+ assert_equal({ days: 2 }, (scalar * 2.days).parts)
+ assert_equal(172800, (scalar * 2.days).value)
+ assert_equal({ days: -2 }, (scalar * -2.days).parts)
+ assert_equal(-172800, (scalar * -2.days).value)
+ end
+
def test_scalar_divide
scalar = ActiveSupport::Duration::Scalar.new(10)
@@ -394,6 +419,15 @@ class DurationTest < ActiveSupport::TestCase
assert_equal "no implicit conversion of String into ActiveSupport::Duration::Scalar", exception.message
end
+ def test_scalar_divide_parts
+ scalar = ActiveSupport::Duration::Scalar.new(10)
+
+ assert_equal({ days: 2 }, (scalar / 5.days).parts)
+ assert_equal(172800, (scalar / 5.days).value)
+ assert_equal({ days: -2 }, (scalar / -5.days).parts)
+ assert_equal(-172800, (scalar / -5.days).value)
+ end
+
def test_twelve_months_equals_one_year
assert_equal 12.months, 1.year
end
diff --git a/activesupport/test/core_ext/module/attribute_accessor_test.rb b/activesupport/test/core_ext/module/attribute_accessor_test.rb
index 464a000d59..9b185e9381 100644
--- a/activesupport/test/core_ext/module/attribute_accessor_test.rb
+++ b/activesupport/test/core_ext/module/attribute_accessor_test.rb
@@ -12,7 +12,14 @@ class ModuleAttributeAccessorTest < ActiveSupport::TestCase
cattr_accessor(:defa) { "default_accessor_value" }
cattr_reader(:defr) { "default_reader_value" }
cattr_writer(:defw) { "default_writer_value" }
+ cattr_accessor(:deff) { false }
cattr_accessor(:quux) { :quux }
+
+ cattr_accessor :def_accessor, default: "default_accessor_value"
+ cattr_reader :def_reader, default: "default_reader_value"
+ cattr_writer :def_writer, default: "default_writer_value"
+ cattr_accessor :def_false, default: false
+ cattr_accessor(:def_priority, default: false) { :no_priority }
end
@class = Class.new
@class.instance_eval { include m }
@@ -24,6 +31,21 @@ class ModuleAttributeAccessorTest < ActiveSupport::TestCase
assert_nil @object.foo
end
+ def test_mattr_default_keyword_arguments
+ assert_equal "default_accessor_value", @module.def_accessor
+ assert_equal "default_reader_value", @module.def_reader
+ assert_equal "default_writer_value", @module.class_variable_get(:@@def_writer)
+ end
+
+ def test_mattr_can_default_to_false
+ assert_equal false, @module.def_false
+ assert_equal false, @module.deff
+ end
+
+ def test_mattr_default_priority
+ assert_equal false, @module.def_priority
+ end
+
def test_should_set_mattr_value
@module.foo = :test
assert_equal :test, @object.foo
@@ -91,9 +113,23 @@ class ModuleAttributeAccessorTest < ActiveSupport::TestCase
assert_equal "default_writer_value", @module.class_variable_get("@@defw")
end
- def test_should_not_invoke_default_value_block_multiple_times
+ def test_method_invocation_should_not_invoke_the_default_block
count = 0
+
@module.cattr_accessor(:defcount) { count += 1 }
+
assert_equal 1, count
+ assert_no_difference "count" do
+ @module.defcount
+ end
+ end
+
+ def test_declaring_multiple_attributes_at_once_invokes_the_block_multiple_times
+ count = 0
+
+ @module.cattr_accessor(:defn1, :defn2) { count += 1 }
+
+ assert_equal 1, @module.defn1
+ assert_equal 2, @module.defn2
end
end
diff --git a/activesupport/test/core_ext/module_test.rb b/activesupport/test/core_ext/module_test.rb
index 085fd6592d..a4d4444d69 100644
--- a/activesupport/test/core_ext/module_test.rb
+++ b/activesupport/test/core_ext/module_test.rb
@@ -348,15 +348,15 @@ class ModuleTest < ActiveSupport::TestCase
assert has_block.hello?
end
- def test_delegate_to_missing_with_method
+ def test_delegate_missing_to_with_method
assert_equal "David", DecoratedTester.new(@david).name
end
- def test_delegate_to_missing_with_reserved_methods
+ def test_delegate_missing_to_with_reserved_methods
assert_equal "David", DecoratedReserved.new(@david).name
end
- def test_delegate_to_missing_does_not_delegate_to_private_methods
+ def test_delegate_missing_to_does_not_delegate_to_private_methods
e = assert_raises(NoMethodError) do
DecoratedReserved.new(@david).private_name
end
@@ -364,7 +364,7 @@ class ModuleTest < ActiveSupport::TestCase
assert_match(/undefined method `private_name' for/, e.message)
end
- def test_delegate_to_missing_does_not_delegate_to_fake_methods
+ def test_delegate_missing_to_does_not_delegate_to_fake_methods
e = assert_raises(NoMethodError) do
DecoratedReserved.new(@david).my_fake_method
end
@@ -372,7 +372,7 @@ class ModuleTest < ActiveSupport::TestCase
assert_match(/undefined method `my_fake_method' for/, e.message)
end
- def test_delegate_to_missing_affects_respond_to
+ def test_delegate_missing_to_affects_respond_to
assert DecoratedTester.new(@david).respond_to?(:name)
assert_not DecoratedTester.new(@david).respond_to?(:private_name)
assert_not DecoratedTester.new(@david).respond_to?(:my_fake_method)
@@ -382,7 +382,7 @@ class ModuleTest < ActiveSupport::TestCase
assert_not DecoratedTester.new(@david).respond_to?(:my_fake_method, true)
end
- def test_delegate_to_missing_respects_superclass_missing
+ def test_delegate_missing_to_respects_superclass_missing
assert_equal 42, DecoratedTester.new(@david).extra_missing
assert_respond_to DecoratedTester.new(@david), :extra_missing
diff --git a/activesupport/test/current_attributes_test.rb b/activesupport/test/current_attributes_test.rb
new file mode 100644
index 0000000000..67ef6ef619
--- /dev/null
+++ b/activesupport/test/current_attributes_test.rb
@@ -0,0 +1,96 @@
+require "abstract_unit"
+
+class CurrentAttributesTest < ActiveSupport::TestCase
+ Person = Struct.new(:name, :time_zone)
+
+ class Current < ActiveSupport::CurrentAttributes
+ attribute :world, :account, :person, :request
+ delegate :time_zone, to: :person
+
+ resets { Time.zone = "UTC" }
+
+ def account=(account)
+ super
+ self.person = "#{account}'s person"
+ end
+
+ def person=(person)
+ super
+ Time.zone = person.try(:time_zone)
+ end
+
+ def request
+ "#{super} something"
+ end
+
+ def intro
+ "#{person.name}, in #{time_zone}"
+ end
+ end
+
+ setup { Current.reset }
+
+ test "read and write attribute" do
+ Current.world = "world/1"
+ assert_equal "world/1", Current.world
+ end
+
+ test "read overwritten attribute method" do
+ Current.request = "request/1"
+ assert_equal "request/1 something", Current.request
+ end
+
+ test "set attribute via overwritten method" do
+ Current.account = "account/1"
+ assert_equal "account/1", Current.account
+ assert_equal "account/1's person", Current.person
+ end
+
+ test "set auxiliary class via overwritten method" do
+ Current.person = Person.new("David", "Central Time (US & Canada)")
+ assert_equal "Central Time (US & Canada)", Time.zone.name
+ end
+
+ test "resets auxiliary class via callback" do
+ Current.person = Person.new("David", "Central Time (US & Canada)")
+ assert_equal "Central Time (US & Canada)", Time.zone.name
+
+ Current.reset
+ assert_equal "UTC", Time.zone.name
+ end
+
+ test "set attribute only via scope" do
+ Current.world = "world/1"
+
+ Current.set(world: "world/2") do
+ assert_equal "world/2", Current.world
+ end
+
+ assert_equal "world/1", Current.world
+ end
+
+ test "set multiple attributes" do
+ Current.world = "world/1"
+ Current.account = "account/1"
+
+ Current.set(world: "world/2", account: "account/2") do
+ assert_equal "world/2", Current.world
+ assert_equal "account/2", Current.account
+ end
+
+ assert_equal "world/1", Current.world
+ assert_equal "account/1", Current.account
+ end
+
+ test "delegation" do
+ Current.person = Person.new("David", "Central Time (US & Canada)")
+ assert_equal "Central Time (US & Canada)", Current.time_zone
+ assert_equal "Central Time (US & Canada)", Current.instance.time_zone
+ end
+
+ test "all methods forward to the instance" do
+ Current.person = Person.new("David", "Central Time (US & Canada)")
+ assert_equal "David, in Central Time (US & Canada)", Current.intro
+ assert_equal "David, in Central Time (US & Canada)", Current.instance.intro
+ end
+end
diff --git a/activesupport/test/dependencies_test.rb b/activesupport/test/dependencies_test.rb
index e38d4e83e5..1ea36418ff 100644
--- a/activesupport/test/dependencies_test.rb
+++ b/activesupport/test/dependencies_test.rb
@@ -104,7 +104,7 @@ class DependenciesTest < ActiveSupport::TestCase
with_loading "dependencies" do
old_warnings, ActiveSupport::Dependencies.warnings_on_first_load = ActiveSupport::Dependencies.warnings_on_first_load, true
filename = "check_warnings"
- expanded = File.expand_path("#{File.dirname(__FILE__)}/dependencies/#{filename}")
+ expanded = File.expand_path("dependencies/#{filename}", __dir__)
$check_warnings_load_count = 0
assert_not ActiveSupport::Dependencies.loaded.include?(expanded)
@@ -293,7 +293,7 @@ class DependenciesTest < ActiveSupport::TestCase
end
def test_doesnt_break_normal_require
- path = File.expand_path("../autoloading_fixtures/load_path", __FILE__)
+ path = File.expand_path("autoloading_fixtures/load_path", __dir__)
original_path = $:.dup
$:.push(path)
with_autoloading_fixtures do
@@ -312,7 +312,7 @@ class DependenciesTest < ActiveSupport::TestCase
end
def test_doesnt_break_normal_require_nested
- path = File.expand_path("../autoloading_fixtures/load_path", __FILE__)
+ path = File.expand_path("autoloading_fixtures/load_path", __dir__)
original_path = $:.dup
$:.push(path)
@@ -332,7 +332,7 @@ class DependenciesTest < ActiveSupport::TestCase
end
def test_require_returns_true_when_file_not_yet_required
- path = File.expand_path("../autoloading_fixtures/load_path", __FILE__)
+ path = File.expand_path("autoloading_fixtures/load_path", __dir__)
original_path = $:.dup
$:.push(path)
@@ -345,7 +345,7 @@ class DependenciesTest < ActiveSupport::TestCase
end
def test_require_returns_true_when_file_not_yet_required_even_when_no_new_constants_added
- path = File.expand_path("../autoloading_fixtures/load_path", __FILE__)
+ path = File.expand_path("autoloading_fixtures/load_path", __dir__)
original_path = $:.dup
$:.push(path)
@@ -359,7 +359,7 @@ class DependenciesTest < ActiveSupport::TestCase
end
def test_require_returns_false_when_file_already_required
- path = File.expand_path("../autoloading_fixtures/load_path", __FILE__)
+ path = File.expand_path("autoloading_fixtures/load_path", __dir__)
original_path = $:.dup
$:.push(path)
@@ -379,7 +379,7 @@ class DependenciesTest < ActiveSupport::TestCase
end
def test_load_returns_true_when_file_found
- path = File.expand_path("../autoloading_fixtures/load_path", __FILE__)
+ path = File.expand_path("autoloading_fixtures/load_path", __dir__)
original_path = $:.dup
$:.push(path)
@@ -438,7 +438,7 @@ class DependenciesTest < ActiveSupport::TestCase
def test_loadable_constants_for_path_should_handle_relative_paths
fake_root = "dependencies"
- relative_root = File.dirname(__FILE__) + "/dependencies"
+ relative_root = File.expand_path("dependencies", __dir__)
["", "/"].each do |suffix|
with_loading fake_root + suffix do
assert_equal ["A::B"], ActiveSupport::Dependencies.loadable_constants_for_path(relative_root + "/a/b")
@@ -463,7 +463,7 @@ class DependenciesTest < ActiveSupport::TestCase
end
def test_loadable_constants_with_load_path_without_trailing_slash
- path = File.dirname(__FILE__) + "/autoloading_fixtures/class_folder/inline_class.rb"
+ path = File.expand_path("autoloading_fixtures/class_folder/inline_class.rb", __dir__)
with_loading "autoloading_fixtures/class/" do
assert_equal [], ActiveSupport::Dependencies.loadable_constants_for_path(path)
end
@@ -991,7 +991,7 @@ class DependenciesTest < ActiveSupport::TestCase
def test_remove_constant_does_not_trigger_loading_autoloads
constant = "ShouldNotBeAutoloaded"
Object.class_eval do
- autoload constant, File.expand_path("../autoloading_fixtures/should_not_be_required", __FILE__)
+ autoload constant, File.expand_path("autoloading_fixtures/should_not_be_required", __dir__)
end
assert_nil ActiveSupport::Dependencies.remove_constant(constant), "Kernel#autoload has been triggered by remove_constant"
diff --git a/activesupport/test/dependencies_test_helpers.rb b/activesupport/test/dependencies_test_helpers.rb
index 9bc63ed89e..451195a143 100644
--- a/activesupport/test/dependencies_test_helpers.rb
+++ b/activesupport/test/dependencies_test_helpers.rb
@@ -1,7 +1,7 @@
module DependenciesTestHelpers
def with_loading(*from)
old_mechanism, ActiveSupport::Dependencies.mechanism = ActiveSupport::Dependencies.mechanism, :load
- this_dir = File.dirname(__FILE__)
+ this_dir = __dir__
parent_dir = File.dirname(this_dir)
path_copy = $LOAD_PATH.dup
$LOAD_PATH.unshift(parent_dir) unless $LOAD_PATH.include?(parent_dir)
diff --git a/activesupport/test/inflector_test.rb b/activesupport/test/inflector_test.rb
index 14bc10513b..ef956eda90 100644
--- a/activesupport/test/inflector_test.rb
+++ b/activesupport/test/inflector_test.rb
@@ -420,6 +420,8 @@ class InflectorTest < ActiveSupport::TestCase
inflect.singular(/es$/, "")
inflect.irregular("el", "los")
+
+ inflect.uncountable("agua")
end
assert_equal("hijos", "hijo".pluralize(:es))
@@ -432,12 +434,17 @@ class InflectorTest < ActiveSupport::TestCase
assert_equal("los", "el".pluralize(:es))
assert_equal("els", "el".pluralize)
+ assert_equal("agua", "agua".pluralize(:es))
+ assert_equal("aguas", "agua".pluralize)
+
ActiveSupport::Inflector.inflections(:es) { |inflect| inflect.clear }
assert ActiveSupport::Inflector.inflections(:es).plurals.empty?
assert ActiveSupport::Inflector.inflections(:es).singulars.empty?
+ assert ActiveSupport::Inflector.inflections(:es).uncountables.empty?
assert !ActiveSupport::Inflector.inflections.plurals.empty?
assert !ActiveSupport::Inflector.inflections.singulars.empty?
+ assert !ActiveSupport::Inflector.inflections.uncountables.empty?
end
def test_clear_all
diff --git a/activesupport/test/message_encryptor_test.rb b/activesupport/test/message_encryptor_test.rb
index 56a436f751..4c3515b5e1 100644
--- a/activesupport/test/message_encryptor_test.rb
+++ b/activesupport/test/message_encryptor_test.rb
@@ -86,20 +86,32 @@ class MessageEncryptorTest < ActiveSupport::TestCase
assert_equal @data, encryptor.decrypt_and_verify(message)
end
+ def test_aead_mode_with_hmac_cbc_cipher_text
+ encryptor = ActiveSupport::MessageEncryptor.new(@secret, cipher: "aes-256-gcm")
+
+ assert_aead_not_decrypted(encryptor, "eHdGeExnZEwvMSt3U3dKaFl1WFo0TjVvYzA0eGpjbm5WSkt5MXlsNzhpZ0ZnbWhBWFlQZTRwaXE1bVJCS2oxMDZhYVp2dVN3V0lNZUlWQ3c2eVhQbnhnVjFmeVVubmhRKzF3WnZyWHVNMDg9LS1HSisyakJVSFlPb05ISzRMaXRzcFdBPT0=--831a1d54a3cda8a0658dc668a03dedcbce13b5ca")
+ end
+
def test_messing_with_aead_values_causes_failures
encryptor = ActiveSupport::MessageEncryptor.new(@secret, cipher: "aes-256-gcm")
text, iv, auth_tag = encryptor.encrypt_and_sign(@data).split("--")
- assert_not_decrypted([iv, text, auth_tag] * "--")
- assert_not_decrypted([munge(text), iv, auth_tag] * "--")
- assert_not_decrypted([text, munge(iv), auth_tag] * "--")
- assert_not_decrypted([text, iv, munge(auth_tag)] * "--")
- assert_not_decrypted([munge(text), munge(iv), munge(auth_tag)] * "--")
- assert_not_decrypted([text, iv] * "--")
- assert_not_decrypted([text, iv, auth_tag[0..-2]] * "--")
+ assert_aead_not_decrypted(encryptor, [iv, text, auth_tag] * "--")
+ assert_aead_not_decrypted(encryptor, [munge(text), iv, auth_tag] * "--")
+ assert_aead_not_decrypted(encryptor, [text, munge(iv), auth_tag] * "--")
+ assert_aead_not_decrypted(encryptor, [text, iv, munge(auth_tag)] * "--")
+ assert_aead_not_decrypted(encryptor, [munge(text), munge(iv), munge(auth_tag)] * "--")
+ assert_aead_not_decrypted(encryptor, [text, iv] * "--")
+ assert_aead_not_decrypted(encryptor, [text, iv, auth_tag[0..-2]] * "--")
end
private
+ def assert_aead_not_decrypted(encryptor, value)
+ assert_raise(ActiveSupport::MessageEncryptor::InvalidMessage) do
+ encryptor.decrypt_and_verify(value)
+ end
+ end
+
def assert_not_decrypted(value)
assert_raise(ActiveSupport::MessageEncryptor::InvalidMessage) do
@encryptor.decrypt_and_verify(@verifier.generate(value))
diff --git a/activesupport/test/number_helper_test.rb b/activesupport/test/number_helper_test.rb
index dc0c34d4e2..4caf1428ea 100644
--- a/activesupport/test/number_helper_test.rb
+++ b/activesupport/test/number_helper_test.rb
@@ -321,12 +321,18 @@ module ActiveSupport
gangster = { hundred: "hundred bucks", million: "thousand quids" }
assert_equal "1 hundred bucks", number_helper.number_to_human(100, units: gangster)
assert_equal "25 hundred bucks", number_helper.number_to_human(2500, units: gangster)
+ assert_equal "1000 hundred bucks", number_helper.number_to_human(100_000, units: gangster)
+ assert_equal "1 thousand quids", number_helper.number_to_human(999_999, units: gangster)
+ assert_equal "1 thousand quids", number_helper.number_to_human(1_000_000, units: gangster)
assert_equal "25 thousand quids", number_helper.number_to_human(25000000, units: gangster)
assert_equal "12300 thousand quids", number_helper.number_to_human(12345000000, units: gangster)
#Spaces are stripped from the resulting string
assert_equal "4", number_helper.number_to_human(4, units: { unit: "", ten: "tens " })
assert_equal "4.5 tens", number_helper.number_to_human(45, units: { unit: "", ten: " tens " })
+
+ #Uses only the provided units and does not try to use larger ones
+ assert_equal "1000 kilometers", number_helper.number_to_human(1_000_000, units: { unit: "meter", thousand: "kilometers" })
end
end
diff --git a/activesupport/test/rescuable_test.rb b/activesupport/test/rescuable_test.rb
index f7eb047d44..3bdd1651e7 100644
--- a/activesupport/test/rescuable_test.rb
+++ b/activesupport/test/rescuable_test.rb
@@ -43,7 +43,9 @@ class Stargate
def dispatch(method)
send(method)
rescue Exception => e
- rescue_with_handler(e)
+ unless rescue_with_handler(e)
+ @result = "unhandled"
+ end
end
def attack
@@ -58,6 +60,26 @@ class Stargate
raise MadRonon.new("dex")
end
+ def crash
+ raise "unhandled RuntimeError"
+ end
+
+ def looped_crash
+ ex1 = StandardError.new("error 1")
+ ex2 = StandardError.new("error 2")
+ begin
+ begin
+ raise ex1
+ rescue
+ # sets the cause on ex2 to be ex1
+ raise ex2
+ end
+ rescue
+ # sets the cause on ex1 to be ex2
+ raise ex1
+ end
+ end
+
def fall_back_to_cause
# This exception is the cause and has a handler.
ronanize
@@ -139,4 +161,14 @@ class RescuableTest < ActiveSupport::TestCase
@stargate.dispatch :fall_back_to_cause
assert_equal "dex", @stargate.result
end
+
+ def test_unhandled_exceptions
+ @stargate.dispatch(:crash)
+ assert_equal "unhandled", @stargate.result
+ end
+
+ def test_rescue_handles_loops_in_exception_cause_chain
+ @stargate.dispatch :looped_crash
+ assert_equal "unhandled", @stargate.result
+ end
end
diff --git a/activesupport/test/test_case_test.rb b/activesupport/test/test_case_test.rb
index af7fc44d66..40dfbe2542 100644
--- a/activesupport/test/test_case_test.rb
+++ b/activesupport/test/test_case_test.rb
@@ -237,9 +237,6 @@ class AssertDifferenceTest < ActiveSupport::TestCase
end
end
-class AlsoDoingNothingTest < ActiveSupport::TestCase
-end
-
# Setup and teardown callbacks.
class SetupAndTeardownTest < ActiveSupport::TestCase
setup :reset_callback_record, :foo
diff --git a/activesupport/test/testing/file_fixtures_test.rb b/activesupport/test/testing/file_fixtures_test.rb
index e44fe58ce9..9f28252c31 100644
--- a/activesupport/test/testing/file_fixtures_test.rb
+++ b/activesupport/test/testing/file_fixtures_test.rb
@@ -3,12 +3,12 @@ require "abstract_unit"
require "pathname"
class FileFixturesTest < ActiveSupport::TestCase
- self.file_fixture_path = File.expand_path("../../file_fixtures", __FILE__)
+ self.file_fixture_path = File.expand_path("../file_fixtures", __dir__)
test "#file_fixture returns Pathname to file fixture" do
path = file_fixture("sample.txt")
assert_kind_of Pathname, path
- assert_match %r{.*/test/file_fixtures/sample.txt$}, path.to_s
+ assert_match %r{.*/test/file_fixtures/sample\.txt$}, path.to_s
end
test "raises an exception when the fixture file does not exist" do
@@ -20,11 +20,11 @@ class FileFixturesTest < ActiveSupport::TestCase
end
class FileFixturesPathnameDirectoryTest < ActiveSupport::TestCase
- self.file_fixture_path = Pathname.new(File.expand_path("../../file_fixtures", __FILE__))
+ self.file_fixture_path = Pathname.new(File.expand_path("../file_fixtures", __dir__))
test "#file_fixture_path returns Pathname to file fixture" do
path = file_fixture("sample.txt")
assert_kind_of Pathname, path
- assert_match %r{.*/test/file_fixtures/sample.txt$}, path.to_s
+ assert_match %r{.*/test/file_fixtures/sample\.txt$}, path.to_s
end
end
diff --git a/activesupport/test/time_travel_test.rb b/activesupport/test/time_travel_test.rb
index e0d3fb0cf5..9d354f14f4 100644
--- a/activesupport/test/time_travel_test.rb
+++ b/activesupport/test/time_travel_test.rb
@@ -99,7 +99,7 @@ class TimeTravelTest < ActiveSupport::TestCase
#noop
end
end
- assert_match(/Calling `travel_to` with a block, when we have previously already made a call to `travel_to`, can lead to confusing time stubbing./, e.message)
+ assert_match(/Calling `travel_to` with a block, when we have previously already made a call to `travel_to`, can lead to confusing time stubbing\./, e.message)
end
end
end
diff --git a/activesupport/test/xml_mini/jdom_engine_test.rb b/activesupport/test/xml_mini/jdom_engine_test.rb
index e783cea67c..fc35ac113b 100644
--- a/activesupport/test/xml_mini/jdom_engine_test.rb
+++ b/activesupport/test/xml_mini/jdom_engine_test.rb
@@ -2,7 +2,7 @@ require_relative "xml_mini_engine_test"
XMLMiniEngineTest.run_with_platform("java") do
class JDOMEngineTest < XMLMiniEngineTest
- FILES_DIR = File.dirname(__FILE__) + "/../fixtures/xml"
+ FILES_DIR = File.expand_path("../fixtures/xml", __dir__)
def test_not_allowed_to_expand_entities_to_files
attack_xml = <<-EOT