diff options
Diffstat (limited to 'activesupport')
192 files changed, 3067 insertions, 1937 deletions
diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md index 6f99155199..ab5237488a 100644 --- a/activesupport/CHANGELOG.md +++ b/activesupport/CHANGELOG.md @@ -1 +1,106 @@ +* 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* + +* Add support for `:offset` and `:zone` to `ActiveSupport::TimeWithZone#change` + + *Andrew White* + +* Add support for `:offset` to `Time#change` + + Fixes #28723. + + *Andrew White* + +* Add `fetch_values` for `HashWithIndifferentAccess` + + The method was originally added to `Hash` in Ruby 2.3.0. + + *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..7302841075 100644 --- a/activesupport/lib/active_support.rb +++ b/activesupport/lib/active_support.rb @@ -22,16 +22,17 @@ #++ require "securerandom" -require "active_support/dependencies/autoload" -require "active_support/version" -require "active_support/logger" -require "active_support/lazy_load_hooks" -require "active_support/core_ext/date_and_time/compatibility" +require_relative "active_support/dependencies/autoload" +require_relative "active_support/version" +require_relative "active_support/logger" +require_relative "active_support/lazy_load_hooks" +require_relative "active_support/core_ext/date_and_time/compatibility" module ActiveSupport extend ActiveSupport::Autoload autoload :Concern + autoload :CurrentAttributes autoload :Dependencies autoload :DescendantsTracker autoload :ExecutionWrapper diff --git a/activesupport/lib/active_support/all.rb b/activesupport/lib/active_support/all.rb index 72a23075af..413314167d 100644 --- a/activesupport/lib/active_support/all.rb +++ b/activesupport/lib/active_support/all.rb @@ -1,3 +1,3 @@ require "active_support" -require "active_support/time" -require "active_support/core_ext" +require_relative "time" +require_relative "core_ext" diff --git a/activesupport/lib/active_support/benchmarkable.rb b/activesupport/lib/active_support/benchmarkable.rb index 70493c8da7..74e39c74e1 100644 --- a/activesupport/lib/active_support/benchmarkable.rb +++ b/activesupport/lib/active_support/benchmarkable.rb @@ -1,5 +1,5 @@ -require "active_support/core_ext/benchmark" -require "active_support/core_ext/hash/keys" +require_relative "core_ext/benchmark" +require_relative "core_ext/hash/keys" module ActiveSupport module Benchmarkable diff --git a/activesupport/lib/active_support/cache.rb b/activesupport/lib/active_support/cache.rb index 4d8c2046e8..f706f8ecf7 100644 --- a/activesupport/lib/active_support/cache.rb +++ b/activesupport/lib/active_support/cache.rb @@ -1,11 +1,11 @@ require "zlib" -require "active_support/core_ext/array/extract_options" -require "active_support/core_ext/array/wrap" -require "active_support/core_ext/module/attribute_accessors" -require "active_support/core_ext/numeric/bytes" -require "active_support/core_ext/numeric/time" -require "active_support/core_ext/object/to_param" -require "active_support/core_ext/string/inflections" +require_relative "core_ext/array/extract_options" +require_relative "core_ext/array/wrap" +require_relative "core_ext/module/attribute_accessors" +require_relative "core_ext/numeric/bytes" +require_relative "core_ext/numeric/time" +require_relative "core_ext/object/to_param" +require_relative "core_ext/string/inflections" module ActiveSupport # See ActiveSupport::Cache::Store for documentation. @@ -75,7 +75,7 @@ module ActiveSupport # # The +key+ argument can also respond to +cache_key+ or +to_param+. def expand_cache_key(key, namespace = nil) - expanded_cache_key = namespace ? "#{namespace}/" : "" + expanded_cache_key = (namespace ? "#{namespace}/" : "").dup if prefix = ENV["RAILS_CACHE_ID"] || ENV["RAILS_APP_VERSION"] expanded_cache_key << "#{prefix}/" @@ -88,17 +88,18 @@ 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 # Obtains the specified cache store class, given the name of the +store+. # Raises an error when the store class cannot be found. def retrieve_store_class(store) - require "active_support/cache/#{store}" + require_relative "cache/#{store}" rescue LoadError => e raise "Could not find cache store adapter for #{store} (#{e})" else @@ -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..bb65da2723 100644 --- a/activesupport/lib/active_support/cache/file_store.rb +++ b/activesupport/lib/active_support/cache/file_store.rb @@ -1,6 +1,6 @@ -require "active_support/core_ext/marshal" -require "active_support/core_ext/file/atomic" -require "active_support/core_ext/string/conversions" +require_relative "../core_ext/marshal" +require_relative "../core_ext/file/atomic" +require_relative "../core_ext/string/conversions" require "uri/common" module ActiveSupport @@ -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..7efe6fe3bf 100644 --- a/activesupport/lib/active_support/cache/mem_cache_store.rb +++ b/activesupport/lib/active_support/cache/mem_cache_store.rb @@ -6,8 +6,8 @@ rescue LoadError => e end require "digest/md5" -require "active_support/core_ext/marshal" -require "active_support/core_ext/array/extract_options" +require_relative "../core_ext/marshal" +require_relative "../core_ext/array/extract_options" module ActiveSupport module Cache @@ -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..2e7e5af0e3 100644 --- a/activesupport/lib/active_support/cache/strategy/local_cache.rb +++ b/activesupport/lib/active_support/cache/strategy/local_cache.rb @@ -1,6 +1,6 @@ -require "active_support/core_ext/object/duplicable" -require "active_support/core_ext/string/inflections" -require "active_support/per_thread_registry" +require_relative "../../core_ext/object/duplicable" +require_relative "../../core_ext/string/inflections" +require_relative "../../per_thread_registry" module ActiveSupport module Cache @@ -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 ea71569ca8..34d62ec3a9 100644 --- a/activesupport/lib/active_support/callbacks.rb +++ b/activesupport/lib/active_support/callbacks.rb @@ -1,11 +1,11 @@ -require "active_support/concern" -require "active_support/descendants_tracker" -require "active_support/core_ext/array/extract_options" -require "active_support/core_ext/class/attribute" -require "active_support/core_ext/kernel/reporting" -require "active_support/core_ext/kernel/singleton_class" -require "active_support/core_ext/string/filters" -require "active_support/deprecation" +require_relative "concern" +require_relative "descendants_tracker" +require_relative "core_ext/array/extract_options" +require_relative "core_ext/class/attribute" +require_relative "core_ext/kernel/reporting" +require_relative "core_ext/kernel/singleton_class" +require_relative "core_ext/string/filters" +require_relative "deprecation" require "thread" module ActiveSupport @@ -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] @@ -512,7 +511,6 @@ module ActiveSupport end end - # An Array with a compile method. class CallbackChain #:nodoc:# include Enumerable @@ -598,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 @@ -664,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/configurable.rb b/activesupport/lib/active_support/configurable.rb index f72a893bcc..ebe4055f91 100644 --- a/activesupport/lib/active_support/configurable.rb +++ b/activesupport/lib/active_support/configurable.rb @@ -1,7 +1,7 @@ -require "active_support/concern" -require "active_support/ordered_options" -require "active_support/core_ext/array/extract_options" -require "active_support/core_ext/regexp" +require_relative "concern" +require_relative "ordered_options" +require_relative "core_ext/array/extract_options" +require_relative "core_ext/regexp" module ActiveSupport # Configurable provides a <tt>config</tt> method to store and retrieve 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/array.rb b/activesupport/lib/active_support/core_ext/array.rb index e908386a1c..3536c7f2bf 100644 --- a/activesupport/lib/active_support/core_ext/array.rb +++ b/activesupport/lib/active_support/core_ext/array.rb @@ -1,7 +1,7 @@ -require "active_support/core_ext/array/wrap" -require "active_support/core_ext/array/access" -require "active_support/core_ext/array/conversions" -require "active_support/core_ext/array/extract_options" -require "active_support/core_ext/array/grouping" -require "active_support/core_ext/array/prepend_and_append" -require "active_support/core_ext/array/inquiry" +require_relative "array/wrap" +require_relative "array/access" +require_relative "array/conversions" +require_relative "array/extract_options" +require_relative "array/grouping" +require_relative "array/prepend_and_append" +require_relative "array/inquiry" diff --git a/activesupport/lib/active_support/core_ext/array/conversions.rb b/activesupport/lib/active_support/core_ext/array/conversions.rb index cac15e1100..57ad32483a 100644 --- a/activesupport/lib/active_support/core_ext/array/conversions.rb +++ b/activesupport/lib/active_support/core_ext/array/conversions.rb @@ -1,8 +1,8 @@ -require "active_support/xml_mini" -require "active_support/core_ext/hash/keys" -require "active_support/core_ext/string/inflections" -require "active_support/core_ext/object/to_param" -require "active_support/core_ext/object/to_query" +require_relative "../../xml_mini" +require_relative "../hash/keys" +require_relative "../string/inflections" +require_relative "../object/to_param" +require_relative "../object/to_query" class Array # Converts the array to a comma-separated sentence where the last element is @@ -179,7 +179,7 @@ class Array # </messages> # def to_xml(options = {}) - require "active_support/builder" unless defined?(Builder) + require_relative "../../builder" unless defined?(Builder) options = options.dup options[:indent] ||= 2 diff --git a/activesupport/lib/active_support/core_ext/array/inquiry.rb b/activesupport/lib/active_support/core_ext/array/inquiry.rb index 66fa5fcd0c..c46b01baf0 100644 --- a/activesupport/lib/active_support/core_ext/array/inquiry.rb +++ b/activesupport/lib/active_support/core_ext/array/inquiry.rb @@ -1,4 +1,4 @@ -require "active_support/array_inquirer" +require_relative "../../array_inquirer" class Array # Wraps the array in an +ArrayInquirer+ object, which gives a friendlier way diff --git a/activesupport/lib/active_support/core_ext/array/prepend_and_append.rb b/activesupport/lib/active_support/core_ext/array/prepend_and_append.rb index 16a6789f8d..88a34128c9 100644 --- a/activesupport/lib/active_support/core_ext/array/prepend_and_append.rb +++ b/activesupport/lib/active_support/core_ext/array/prepend_and_append.rb @@ -1,7 +1,7 @@ class Array # The human way of thinking about adding stuff to the end of a list is with append. - alias_method :append, :<< + alias_method :append, :push unless [].respond_to?(:append) # The human way of thinking about adding stuff to the beginning of a list is with prepend. - alias_method :prepend, :unshift + alias_method :prepend, :unshift unless [].respond_to?(:prepend) end diff --git a/activesupport/lib/active_support/core_ext/big_decimal.rb b/activesupport/lib/active_support/core_ext/big_decimal.rb index 7b4f87f10e..7e396cac3c 100644 --- a/activesupport/lib/active_support/core_ext/big_decimal.rb +++ b/activesupport/lib/active_support/core_ext/big_decimal.rb @@ -1 +1 @@ -require "active_support/core_ext/big_decimal/conversions" +require_relative "big_decimal/conversions" diff --git a/activesupport/lib/active_support/core_ext/class.rb b/activesupport/lib/active_support/core_ext/class.rb index 6a19e862d0..66dd1c939a 100644 --- a/activesupport/lib/active_support/core_ext/class.rb +++ b/activesupport/lib/active_support/core_ext/class.rb @@ -1,2 +1,2 @@ -require "active_support/core_ext/class/attribute" -require "active_support/core_ext/class/subclasses" +require_relative "class/attribute" +require_relative "class/subclasses" diff --git a/activesupport/lib/active_support/core_ext/class/attribute.rb b/activesupport/lib/active_support/core_ext/class/attribute.rb index ba422f9071..26b826b39b 100644 --- a/activesupport/lib/active_support/core_ext/class/attribute.rb +++ b/activesupport/lib/active_support/core_ext/class/attribute.rb @@ -1,6 +1,6 @@ -require "active_support/core_ext/kernel/singleton_class" -require "active_support/core_ext/module/remove_method" -require "active_support/core_ext/array/extract_options" +require_relative "../kernel/singleton_class" +require_relative "../module/remove_method" +require_relative "../array/extract_options" class Class # Declare a class-level attribute whose value is inheritable by subclasses. @@ -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/class/attribute_accessors.rb b/activesupport/lib/active_support/core_ext/class/attribute_accessors.rb index 0f767925ed..eebf2e294a 100644 --- a/activesupport/lib/active_support/core_ext/class/attribute_accessors.rb +++ b/activesupport/lib/active_support/core_ext/class/attribute_accessors.rb @@ -1,4 +1,4 @@ # cattr_* became mattr_* aliases in 7dfbd91b0780fbd6a1dd9bfbc176e10894871d2d, # but we keep this around for libraries that directly require it knowing they # want cattr_*. No need to deprecate. -require "active_support/core_ext/module/attribute_accessors" +require_relative "../module/attribute_accessors" diff --git a/activesupport/lib/active_support/core_ext/class/subclasses.rb b/activesupport/lib/active_support/core_ext/class/subclasses.rb index 62397d9508..545ad7fb3e 100644 --- a/activesupport/lib/active_support/core_ext/class/subclasses.rb +++ b/activesupport/lib/active_support/core_ext/class/subclasses.rb @@ -1,5 +1,5 @@ -require "active_support/core_ext/module/anonymous" -require "active_support/core_ext/module/reachable" +require_relative "../module/anonymous" +require_relative "../module/reachable" class Class begin diff --git a/activesupport/lib/active_support/core_ext/date.rb b/activesupport/lib/active_support/core_ext/date.rb index 4f66804da2..aba72c732f 100644 --- a/activesupport/lib/active_support/core_ext/date.rb +++ b/activesupport/lib/active_support/core_ext/date.rb @@ -1,5 +1,5 @@ -require "active_support/core_ext/date/acts_like" -require "active_support/core_ext/date/blank" -require "active_support/core_ext/date/calculations" -require "active_support/core_ext/date/conversions" -require "active_support/core_ext/date/zones" +require_relative "date/acts_like" +require_relative "date/blank" +require_relative "date/calculations" +require_relative "date/conversions" +require_relative "date/zones" diff --git a/activesupport/lib/active_support/core_ext/date/acts_like.rb b/activesupport/lib/active_support/core_ext/date/acts_like.rb index 46fe99ad47..dfea3e3f09 100644 --- a/activesupport/lib/active_support/core_ext/date/acts_like.rb +++ b/activesupport/lib/active_support/core_ext/date/acts_like.rb @@ -1,4 +1,4 @@ -require "active_support/core_ext/object/acts_like" +require_relative "../object/acts_like" class Date # Duck-types as a Date-like class. See Object#acts_like?. diff --git a/activesupport/lib/active_support/core_ext/date/calculations.rb b/activesupport/lib/active_support/core_ext/date/calculations.rb index d6f60cac04..24d3721d48 100644 --- a/activesupport/lib/active_support/core_ext/date/calculations.rb +++ b/activesupport/lib/active_support/core_ext/date/calculations.rb @@ -1,9 +1,9 @@ require "date" -require "active_support/duration" -require "active_support/core_ext/object/acts_like" -require "active_support/core_ext/date/zones" -require "active_support/core_ext/time/zones" -require "active_support/core_ext/date_and_time/calculations" +require_relative "../../duration" +require_relative "../object/acts_like" +require_relative "zones" +require_relative "../time/zones" +require_relative "../date_and_time/calculations" class Date include DateAndTime::Calculations diff --git a/activesupport/lib/active_support/core_ext/date/conversions.rb b/activesupport/lib/active_support/core_ext/date/conversions.rb index d553406dff..7971d39fb7 100644 --- a/activesupport/lib/active_support/core_ext/date/conversions.rb +++ b/activesupport/lib/active_support/core_ext/date/conversions.rb @@ -1,7 +1,7 @@ require "date" -require "active_support/inflector/methods" -require "active_support/core_ext/date/zones" -require "active_support/core_ext/module/remove_method" +require_relative "../../inflector/methods" +require_relative "zones" +require_relative "../module/remove_method" class Date DATE_FORMATS = { @@ -79,6 +79,9 @@ class Date # date.to_time(:local) # => 2007-11-10 00:00:00 0800 # # date.to_time(:utc) # => 2007-11-10 00:00:00 UTC + # + # NOTE: The :local timezone is Ruby's *process* timezone, i.e. ENV['TZ']. + # If the *application's* timezone is needed, then use +in_time_zone+ instead. def to_time(form = :local) raise ArgumentError, "Expected :local or :utc, got #{form.inspect}." unless [:local, :utc].include?(form) ::Time.send(form, year, month, day) diff --git a/activesupport/lib/active_support/core_ext/date/zones.rb b/activesupport/lib/active_support/core_ext/date/zones.rb index da23fe4892..d94c7bcfc2 100644 --- a/activesupport/lib/active_support/core_ext/date/zones.rb +++ b/activesupport/lib/active_support/core_ext/date/zones.rb @@ -1,5 +1,5 @@ require "date" -require "active_support/core_ext/date_and_time/zones" +require_relative "../date_and_time/zones" class Date include DateAndTime::Zones 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..cf01a4542f 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 @@ -1,4 +1,4 @@ -require "active_support/core_ext/object/try" +require_relative "../object/try" module DateAndTime module Calculations @@ -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..bb9908df61 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 @@ -1,4 +1,4 @@ -require "active_support/core_ext/module/attribute_accessors" +require_relative "../module/attribute_accessors" module DateAndTime module Compatibility @@ -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/date_time.rb b/activesupport/lib/active_support/core_ext/date_time.rb index 6fd498f864..ce35026a71 100644 --- a/activesupport/lib/active_support/core_ext/date_time.rb +++ b/activesupport/lib/active_support/core_ext/date_time.rb @@ -1,5 +1,5 @@ -require "active_support/core_ext/date_time/acts_like" -require "active_support/core_ext/date_time/blank" -require "active_support/core_ext/date_time/calculations" -require "active_support/core_ext/date_time/compatibility" -require "active_support/core_ext/date_time/conversions" +require_relative "date_time/acts_like" +require_relative "date_time/blank" +require_relative "date_time/calculations" +require_relative "date_time/compatibility" +require_relative "date_time/conversions" diff --git a/activesupport/lib/active_support/core_ext/date_time/acts_like.rb b/activesupport/lib/active_support/core_ext/date_time/acts_like.rb index 6f50f55a53..f0356ee6a0 100644 --- a/activesupport/lib/active_support/core_ext/date_time/acts_like.rb +++ b/activesupport/lib/active_support/core_ext/date_time/acts_like.rb @@ -1,5 +1,5 @@ require "date" -require "active_support/core_ext/object/acts_like" +require_relative "../object/acts_like" class DateTime # Duck-types as a Date-like class. See Object#acts_like?. diff --git a/activesupport/lib/active_support/core_ext/date_time/compatibility.rb b/activesupport/lib/active_support/core_ext/date_time/compatibility.rb index eb8b8b2c65..fe255c087f 100644 --- a/activesupport/lib/active_support/core_ext/date_time/compatibility.rb +++ b/activesupport/lib/active_support/core_ext/date_time/compatibility.rb @@ -1,4 +1,5 @@ -require "active_support/core_ext/date_and_time/compatibility" +require_relative "../date_and_time/compatibility" +require_relative "../module/remove_method" class DateTime include DateAndTime::Compatibility diff --git a/activesupport/lib/active_support/core_ext/date_time/conversions.rb b/activesupport/lib/active_support/core_ext/date_time/conversions.rb index d9b3743858..36bfd28f31 100644 --- a/activesupport/lib/active_support/core_ext/date_time/conversions.rb +++ b/activesupport/lib/active_support/core_ext/date_time/conversions.rb @@ -1,8 +1,8 @@ require "date" -require "active_support/inflector/methods" -require "active_support/core_ext/time/conversions" -require "active_support/core_ext/date_time/calculations" -require "active_support/values/time_zone" +require_relative "../../inflector/methods" +require_relative "../time/conversions" +require_relative "calculations" +require_relative "../../values/time_zone" class DateTime # Convert to a formatted string. See Time::DATE_FORMATS for predefined formats. diff --git a/activesupport/lib/active_support/core_ext/enumerable.rb b/activesupport/lib/active_support/core_ext/enumerable.rb index 90d7d2947f..3a4ae6cb8b 100644 --- a/activesupport/lib/active_support/core_ext/enumerable.rb +++ b/activesupport/lib/active_support/core_ext/enumerable.rb @@ -1,28 +1,50 @@ module Enumerable - # Calculates a sum from the elements. + # Enumerable#sum was added in Ruby 2.4, but it only works with Numeric elements + # when we omit an identity. # - # payments.sum { |p| p.price * p.tax_rate } - # payments.sum(&:price) - # - # The latter is a shortcut for: - # - # payments.inject(0) { |sum, p| sum + p.price } - # - # It can also calculate the sum without the use of a block. - # - # [5, 15, 10].sum # => 30 - # ['foo', 'bar'].sum # => "foobar" - # [[1, 2], [3, 1, 5]].sum # => [1, 2, 3, 1, 5] - # - # The default sum of an empty list is zero. You can override this default: - # - # [].sum(Payment.new(0)) { |i| i.amount } # => Payment.new(0) - def sum(identity = nil, &block) - if block_given? - map(&block).sum(identity) - else - sum = identity ? inject(identity, :+) : inject(:+) - sum || identity || 0 + # We tried shimming it to attempt the fast native method, rescue TypeError, + # and fall back to the compatible implementation, but that's much slower than + # just calling the compat method in the first place. + if Enumerable.instance_methods(false).include?(:sum) && !((?a..?b).sum rescue false) + # We can't use Refinements here because Refinements with Module which will be prepended + # doesn't work well https://bugs.ruby-lang.org/issues/13446 + alias :_original_sum_with_required_identity :sum + private :_original_sum_with_required_identity + # Calculates a sum from the elements. + # + # payments.sum { |p| p.price * p.tax_rate } + # payments.sum(&:price) + # + # The latter is a shortcut for: + # + # payments.inject(0) { |sum, p| sum + p.price } + # + # It can also calculate the sum without the use of a block. + # + # [5, 15, 10].sum # => 30 + # ['foo', 'bar'].sum # => "foobar" + # [[1, 2], [3, 1, 5]].sum # => [1, 2, 3, 1, 5] + # + # The default sum of an empty list is zero. You can override this default: + # + # [].sum(Payment.new(0)) { |i| i.amount } # => Payment.new(0) + def sum(identity = nil, &block) + if identity + _original_sum_with_required_identity(identity, &block) + elsif block_given? + map(&block).sum(identity) + else + inject(:+) || 0 + end + end + else + def sum(identity = nil, &block) + if block_given? + map(&block).sum(identity) + else + sum = identity ? inject(identity, :+) : inject(:+) + sum || identity || 0 + end end end diff --git a/activesupport/lib/active_support/core_ext/file.rb b/activesupport/lib/active_support/core_ext/file.rb index 6d99bad2af..ea3267f5da 100644 --- a/activesupport/lib/active_support/core_ext/file.rb +++ b/activesupport/lib/active_support/core_ext/file.rb @@ -1 +1 @@ -require "active_support/core_ext/file/atomic" +require_relative "file/atomic" diff --git a/activesupport/lib/active_support/core_ext/hash.rb b/activesupport/lib/active_support/core_ext/hash.rb index c819307e8a..6122ac57ba 100644 --- a/activesupport/lib/active_support/core_ext/hash.rb +++ b/activesupport/lib/active_support/core_ext/hash.rb @@ -1,9 +1,9 @@ -require "active_support/core_ext/hash/compact" -require "active_support/core_ext/hash/conversions" -require "active_support/core_ext/hash/deep_merge" -require "active_support/core_ext/hash/except" -require "active_support/core_ext/hash/indifferent_access" -require "active_support/core_ext/hash/keys" -require "active_support/core_ext/hash/reverse_merge" -require "active_support/core_ext/hash/slice" -require "active_support/core_ext/hash/transform_values" +require_relative "hash/compact" +require_relative "hash/conversions" +require_relative "hash/deep_merge" +require_relative "hash/except" +require_relative "hash/indifferent_access" +require_relative "hash/keys" +require_relative "hash/reverse_merge" +require_relative "hash/slice" +require_relative "hash/transform_values" diff --git a/activesupport/lib/active_support/core_ext/hash/conversions.rb b/activesupport/lib/active_support/core_ext/hash/conversions.rb index 2a58a7f1ca..713711e16a 100644 --- a/activesupport/lib/active_support/core_ext/hash/conversions.rb +++ b/activesupport/lib/active_support/core_ext/hash/conversions.rb @@ -1,11 +1,11 @@ -require "active_support/xml_mini" -require "active_support/time" -require "active_support/core_ext/object/blank" -require "active_support/core_ext/object/to_param" -require "active_support/core_ext/object/to_query" -require "active_support/core_ext/array/wrap" -require "active_support/core_ext/hash/reverse_merge" -require "active_support/core_ext/string/inflections" +require_relative "../../xml_mini" +require_relative "../../time" +require_relative "../object/blank" +require_relative "../object/to_param" +require_relative "../object/to_query" +require_relative "../array/wrap" +require_relative "reverse_merge" +require_relative "../string/inflections" class Hash # Returns a string containing an XML representation of its receiver: @@ -71,7 +71,7 @@ class Hash # configure your own builder with the <tt>:builder</tt> option. The method also accepts # options like <tt>:dasherize</tt> and friends, they are forwarded to the builder. def to_xml(options = {}) - require "active_support/builder" unless defined?(Builder) + require_relative "../../builder" unless defined?(Builder) options = options.dup options[:indent] ||= 2 diff --git a/activesupport/lib/active_support/core_ext/hash/indifferent_access.rb b/activesupport/lib/active_support/core_ext/hash/indifferent_access.rb index 3e1ccecb6c..4b7fe34cc9 100644 --- a/activesupport/lib/active_support/core_ext/hash/indifferent_access.rb +++ b/activesupport/lib/active_support/core_ext/hash/indifferent_access.rb @@ -1,4 +1,4 @@ -require "active_support/hash_with_indifferent_access" +require_relative "../../hash_with_indifferent_access" class Hash # Returns an <tt>ActiveSupport::HashWithIndifferentAccess</tt> out of its receiver: diff --git a/activesupport/lib/active_support/core_ext/integer.rb b/activesupport/lib/active_support/core_ext/integer.rb index 8f0c55f9d3..e7c1aae980 100644 --- a/activesupport/lib/active_support/core_ext/integer.rb +++ b/activesupport/lib/active_support/core_ext/integer.rb @@ -1,3 +1,3 @@ -require "active_support/core_ext/integer/multiple" -require "active_support/core_ext/integer/inflections" -require "active_support/core_ext/integer/time" +require_relative "integer/multiple" +require_relative "integer/inflections" +require_relative "integer/time" diff --git a/activesupport/lib/active_support/core_ext/integer/inflections.rb b/activesupport/lib/active_support/core_ext/integer/inflections.rb index bc21b65533..0b2ada8b22 100644 --- a/activesupport/lib/active_support/core_ext/integer/inflections.rb +++ b/activesupport/lib/active_support/core_ext/integer/inflections.rb @@ -1,4 +1,4 @@ -require "active_support/inflector" +require_relative "../../inflector" class Integer # Ordinalize turns a number into an ordinal string used to denote the diff --git a/activesupport/lib/active_support/core_ext/integer/time.rb b/activesupport/lib/active_support/core_ext/integer/time.rb index 74baae3639..d5625dfd1c 100644 --- a/activesupport/lib/active_support/core_ext/integer/time.rb +++ b/activesupport/lib/active_support/core_ext/integer/time.rb @@ -1,5 +1,5 @@ -require "active_support/duration" -require "active_support/core_ext/numeric/time" +require_relative "../../duration" +require_relative "../numeric/time" class Integer # Enables the use of time calculations and declarations, like <tt>45.minutes + diff --git a/activesupport/lib/active_support/core_ext/kernel.rb b/activesupport/lib/active_support/core_ext/kernel.rb index 3d41ff7876..26142b107b 100644 --- a/activesupport/lib/active_support/core_ext/kernel.rb +++ b/activesupport/lib/active_support/core_ext/kernel.rb @@ -1,4 +1,4 @@ -require "active_support/core_ext/kernel/agnostics" -require "active_support/core_ext/kernel/concern" -require "active_support/core_ext/kernel/reporting" -require "active_support/core_ext/kernel/singleton_class" +require_relative "kernel/agnostics" +require_relative "kernel/concern" +require_relative "kernel/reporting" +require_relative "kernel/singleton_class" diff --git a/activesupport/lib/active_support/core_ext/kernel/concern.rb b/activesupport/lib/active_support/core_ext/kernel/concern.rb index 307a7f7a63..535f0d21df 100644 --- a/activesupport/lib/active_support/core_ext/kernel/concern.rb +++ b/activesupport/lib/active_support/core_ext/kernel/concern.rb @@ -1,4 +1,4 @@ -require "active_support/core_ext/module/concerning" +require_relative "../module/concerning" module Kernel module_function diff --git a/activesupport/lib/active_support/core_ext/module.rb b/activesupport/lib/active_support/core_ext/module.rb index 2930255557..3ee394edd9 100644 --- a/activesupport/lib/active_support/core_ext/module.rb +++ b/activesupport/lib/active_support/core_ext/module.rb @@ -1,11 +1,11 @@ -require "active_support/core_ext/module/aliasing" -require "active_support/core_ext/module/introspection" -require "active_support/core_ext/module/anonymous" -require "active_support/core_ext/module/reachable" -require "active_support/core_ext/module/attribute_accessors" -require "active_support/core_ext/module/attribute_accessors_per_thread" -require "active_support/core_ext/module/attr_internal" -require "active_support/core_ext/module/concerning" -require "active_support/core_ext/module/delegation" -require "active_support/core_ext/module/deprecation" -require "active_support/core_ext/module/remove_method" +require_relative "module/aliasing" +require_relative "module/introspection" +require_relative "module/anonymous" +require_relative "module/reachable" +require_relative "module/attribute_accessors" +require_relative "module/attribute_accessors_per_thread" +require_relative "module/attr_internal" +require_relative "module/concerning" +require_relative "module/delegation" +require_relative "module/deprecation" +require_relative "module/remove_method" 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..e978bd41d6 100644 --- a/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb +++ b/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb @@ -1,5 +1,5 @@ -require "active_support/core_ext/array/extract_options" -require "active_support/core_ext/regexp" +require_relative "../array/extract_options" +require_relative "../regexp" # Extends the module object with class/module and instance accessors for # class/module attributes, just like the native attr* accessors for instance @@ -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/attribute_accessors_per_thread.rb b/activesupport/lib/active_support/core_ext/module/attribute_accessors_per_thread.rb index 1e82b4acc2..9ef834fa6f 100644 --- a/activesupport/lib/active_support/core_ext/module/attribute_accessors_per_thread.rb +++ b/activesupport/lib/active_support/core_ext/module/attribute_accessors_per_thread.rb @@ -1,5 +1,5 @@ -require "active_support/core_ext/array/extract_options" -require "active_support/core_ext/regexp" +require_relative "../array/extract_options" +require_relative "../regexp" # Extends the module object with class/module and instance accessors for # class/module attributes, just like the native attr* accessors for instance diff --git a/activesupport/lib/active_support/core_ext/module/concerning.rb b/activesupport/lib/active_support/core_ext/module/concerning.rb index 97b0a382ce..eb821aba60 100644 --- a/activesupport/lib/active_support/core_ext/module/concerning.rb +++ b/activesupport/lib/active_support/core_ext/module/concerning.rb @@ -1,4 +1,4 @@ -require "active_support/concern" +require_relative "../../concern" class Module # = Bite-sized separation of concerns diff --git a/activesupport/lib/active_support/core_ext/module/delegation.rb b/activesupport/lib/active_support/core_ext/module/delegation.rb index cdf27f49ad..13bb8070ae 100644 --- a/activesupport/lib/active_support/core_ext/module/delegation.rb +++ b/activesupport/lib/active_support/core_ext/module/delegation.rb @@ -1,5 +1,5 @@ require "set" -require "active_support/core_ext/regexp" +require_relative "../regexp" class Module # Error generated by +delegate+ when a method is called on +nil+ and +allow_nil+ @@ -174,7 +174,7 @@ class Module to = to.to_s to = "self.#{to}" if DELEGATION_RESERVED_METHOD_NAMES.include?(to) - methods.each do |method| + methods.map do |method| # Attribute writer methods only accept one argument. Makes sure []= # methods still accept two arguments. definition = /[^\]]=$/.match?(method) ? "arg" : "*args, &block" @@ -219,55 +219,53 @@ 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) module_eval <<-RUBY, __FILE__, __LINE__ + 1 def respond_to_missing?(name, include_private = false) - #{target}.respond_to?(name, include_private) + # It may look like an oversight, but we deliberately do not pass + # +include_private+, because they do not get delegated. + + #{target}.respond_to?(name) || super end def method_missing(method, *args, &block) diff --git a/activesupport/lib/active_support/core_ext/module/introspection.rb b/activesupport/lib/active_support/core_ext/module/introspection.rb index ca20a6d4c5..4a7930792c 100644 --- a/activesupport/lib/active_support/core_ext/module/introspection.rb +++ b/activesupport/lib/active_support/core_ext/module/introspection.rb @@ -1,4 +1,4 @@ -require "active_support/inflector" +require_relative "../../inflector" class Module # Returns the name of the module containing this one. diff --git a/activesupport/lib/active_support/core_ext/module/reachable.rb b/activesupport/lib/active_support/core_ext/module/reachable.rb index b89a38f26c..0f78999634 100644 --- a/activesupport/lib/active_support/core_ext/module/reachable.rb +++ b/activesupport/lib/active_support/core_ext/module/reachable.rb @@ -1,5 +1,5 @@ -require "active_support/core_ext/module/anonymous" -require "active_support/core_ext/string/inflections" +require_relative "anonymous" +require_relative "../string/inflections" class Module def reachable? #:nodoc: diff --git a/activesupport/lib/active_support/core_ext/numeric.rb b/activesupport/lib/active_support/core_ext/numeric.rb index 6062f9e3a8..e10d7708bd 100644 --- a/activesupport/lib/active_support/core_ext/numeric.rb +++ b/activesupport/lib/active_support/core_ext/numeric.rb @@ -1,4 +1,4 @@ -require "active_support/core_ext/numeric/bytes" -require "active_support/core_ext/numeric/time" -require "active_support/core_ext/numeric/inquiry" -require "active_support/core_ext/numeric/conversions" +require_relative "numeric/bytes" +require_relative "numeric/time" +require_relative "numeric/inquiry" +require_relative "numeric/conversions" diff --git a/activesupport/lib/active_support/core_ext/numeric/conversions.rb b/activesupport/lib/active_support/core_ext/numeric/conversions.rb index 4f6621693e..397e32ee8e 100644 --- a/activesupport/lib/active_support/core_ext/numeric/conversions.rb +++ b/activesupport/lib/active_support/core_ext/numeric/conversions.rb @@ -1,6 +1,6 @@ -require "active_support/core_ext/big_decimal/conversions" -require "active_support/number_helper" -require "active_support/core_ext/module/deprecation" +require_relative "../big_decimal/conversions" +require_relative "../../number_helper" +require_relative "../module/deprecation" module ActiveSupport::NumericWithFormat # Provides options for converting numbers into formatted strings. diff --git a/activesupport/lib/active_support/core_ext/numeric/time.rb b/activesupport/lib/active_support/core_ext/numeric/time.rb index 2e6c70d418..cf002552fb 100644 --- a/activesupport/lib/active_support/core_ext/numeric/time.rb +++ b/activesupport/lib/active_support/core_ext/numeric/time.rb @@ -1,8 +1,8 @@ -require "active_support/duration" -require "active_support/core_ext/time/calculations" -require "active_support/core_ext/time/acts_like" -require "active_support/core_ext/date/calculations" -require "active_support/core_ext/date/acts_like" +require_relative "../../duration" +require_relative "../time/calculations" +require_relative "../time/acts_like" +require_relative "../date/calculations" +require_relative "../date/acts_like" class Numeric # Enables the use of time calculations and declarations, like 45.minutes + 2.hours + 4.years. diff --git a/activesupport/lib/active_support/core_ext/object.rb b/activesupport/lib/active_support/core_ext/object.rb index 58bbf78601..993cc6a059 100644 --- a/activesupport/lib/active_support/core_ext/object.rb +++ b/activesupport/lib/active_support/core_ext/object.rb @@ -1,14 +1,14 @@ -require "active_support/core_ext/object/acts_like" -require "active_support/core_ext/object/blank" -require "active_support/core_ext/object/duplicable" -require "active_support/core_ext/object/deep_dup" -require "active_support/core_ext/object/try" -require "active_support/core_ext/object/inclusion" +require_relative "object/acts_like" +require_relative "object/blank" +require_relative "object/duplicable" +require_relative "object/deep_dup" +require_relative "object/try" +require_relative "object/inclusion" -require "active_support/core_ext/object/conversions" -require "active_support/core_ext/object/instance_variables" +require_relative "object/conversions" +require_relative "object/instance_variables" -require "active_support/core_ext/object/json" -require "active_support/core_ext/object/to_param" -require "active_support/core_ext/object/to_query" -require "active_support/core_ext/object/with_options" +require_relative "object/json" +require_relative "object/to_param" +require_relative "object/to_query" +require_relative "object/with_options" diff --git a/activesupport/lib/active_support/core_ext/object/blank.rb b/activesupport/lib/active_support/core_ext/object/blank.rb index bdb50ee291..c37c2649d6 100644 --- a/activesupport/lib/active_support/core_ext/object/blank.rb +++ b/activesupport/lib/active_support/core_ext/object/blank.rb @@ -1,4 +1,4 @@ -require "active_support/core_ext/regexp" +require_relative "../regexp" class Object # An object is blank if it's false, empty, or a whitespace string. diff --git a/activesupport/lib/active_support/core_ext/object/conversions.rb b/activesupport/lib/active_support/core_ext/object/conversions.rb index 918ebcdc9f..9d3bdbc528 100644 --- a/activesupport/lib/active_support/core_ext/object/conversions.rb +++ b/activesupport/lib/active_support/core_ext/object/conversions.rb @@ -1,4 +1,4 @@ -require "active_support/core_ext/object/to_param" -require "active_support/core_ext/object/to_query" -require "active_support/core_ext/array/conversions" -require "active_support/core_ext/hash/conversions" +require_relative "to_param" +require_relative "to_query" +require_relative "../array/conversions" +require_relative "../hash/conversions" diff --git a/activesupport/lib/active_support/core_ext/object/deep_dup.rb b/activesupport/lib/active_support/core_ext/object/deep_dup.rb index 5ac649e552..d966a29408 100644 --- a/activesupport/lib/active_support/core_ext/object/deep_dup.rb +++ b/activesupport/lib/active_support/core_ext/object/deep_dup.rb @@ -1,4 +1,4 @@ -require "active_support/core_ext/object/duplicable" +require_relative "duplicable" class Object # Returns a deep copy of object if it's duplicable. If it's diff --git a/activesupport/lib/active_support/core_ext/object/json.rb b/activesupport/lib/active_support/core_ext/object/json.rb index 1c4d181443..d54373d5f2 100644 --- a/activesupport/lib/active_support/core_ext/object/json.rb +++ b/activesupport/lib/active_support/core_ext/object/json.rb @@ -3,14 +3,14 @@ require "json" require "bigdecimal" require "uri/generic" require "pathname" -require "active_support/core_ext/big_decimal/conversions" # for #to_s -require "active_support/core_ext/hash/except" -require "active_support/core_ext/hash/slice" -require "active_support/core_ext/object/instance_variables" +require_relative "../big_decimal/conversions" # for #to_s +require_relative "../hash/except" +require_relative "../hash/slice" +require_relative "instance_variables" require "time" -require "active_support/core_ext/time/conversions" -require "active_support/core_ext/date_time/conversions" -require "active_support/core_ext/date/conversions" +require_relative "../time/conversions" +require_relative "../date_time/conversions" +require_relative "../date/conversions" # The JSON gem adds a few modules to Ruby core classes containing :to_json definition, overwriting # their default behavior. That said, we need to define the basic to_json method in all of them, diff --git a/activesupport/lib/active_support/core_ext/object/to_param.rb b/activesupport/lib/active_support/core_ext/object/to_param.rb index 5eeaf03163..d39727ce00 100644 --- a/activesupport/lib/active_support/core_ext/object/to_param.rb +++ b/activesupport/lib/active_support/core_ext/object/to_param.rb @@ -1 +1 @@ -require "active_support/core_ext/object/to_query" +require_relative "to_query" diff --git a/activesupport/lib/active_support/core_ext/object/with_options.rb b/activesupport/lib/active_support/core_ext/object/with_options.rb index 3a44e08630..68a8da25ff 100644 --- a/activesupport/lib/active_support/core_ext/object/with_options.rb +++ b/activesupport/lib/active_support/core_ext/object/with_options.rb @@ -1,4 +1,4 @@ -require "active_support/option_merger" +require_relative "../../option_merger" class Object # An elegant way to factor duplication out of options passed to a series of diff --git a/activesupport/lib/active_support/core_ext/range.rb b/activesupport/lib/active_support/core_ext/range.rb index 3190e3ff76..3e17780b76 100644 --- a/activesupport/lib/active_support/core_ext/range.rb +++ b/activesupport/lib/active_support/core_ext/range.rb @@ -1,4 +1,4 @@ -require "active_support/core_ext/range/conversions" -require "active_support/core_ext/range/include_range" -require "active_support/core_ext/range/overlaps" -require "active_support/core_ext/range/each" +require_relative "range/conversions" +require_relative "range/include_range" +require_relative "range/overlaps" +require_relative "range/each" diff --git a/activesupport/lib/active_support/core_ext/string.rb b/activesupport/lib/active_support/core_ext/string.rb index 4cb3200875..b16b35144e 100644 --- a/activesupport/lib/active_support/core_ext/string.rb +++ b/activesupport/lib/active_support/core_ext/string.rb @@ -1,13 +1,13 @@ -require "active_support/core_ext/string/conversions" -require "active_support/core_ext/string/filters" -require "active_support/core_ext/string/multibyte" -require "active_support/core_ext/string/starts_ends_with" -require "active_support/core_ext/string/inflections" -require "active_support/core_ext/string/access" -require "active_support/core_ext/string/behavior" -require "active_support/core_ext/string/output_safety" -require "active_support/core_ext/string/exclude" -require "active_support/core_ext/string/strip" -require "active_support/core_ext/string/inquiry" -require "active_support/core_ext/string/indent" -require "active_support/core_ext/string/zones" +require_relative "string/conversions" +require_relative "string/filters" +require_relative "string/multibyte" +require_relative "string/starts_ends_with" +require_relative "string/inflections" +require_relative "string/access" +require_relative "string/behavior" +require_relative "string/output_safety" +require_relative "string/exclude" +require_relative "string/strip" +require_relative "string/inquiry" +require_relative "string/indent" +require_relative "string/zones" diff --git a/activesupport/lib/active_support/core_ext/string/conversions.rb b/activesupport/lib/active_support/core_ext/string/conversions.rb index 221b4969cc..383d93e2cd 100644 --- a/activesupport/lib/active_support/core_ext/string/conversions.rb +++ b/activesupport/lib/active_support/core_ext/string/conversions.rb @@ -1,5 +1,5 @@ require "date" -require "active_support/core_ext/time/calculations" +require_relative "../time/calculations" class String # Converts a string to a Time value. diff --git a/activesupport/lib/active_support/core_ext/string/inflections.rb b/activesupport/lib/active_support/core_ext/string/inflections.rb index b27cfe53f4..70c732ca44 100644 --- a/activesupport/lib/active_support/core_ext/string/inflections.rb +++ b/activesupport/lib/active_support/core_ext/string/inflections.rb @@ -1,5 +1,5 @@ -require "active_support/inflector/methods" -require "active_support/inflector/transliterate" +require_relative "../../inflector/methods" +require_relative "../../inflector/transliterate" # String inflections define new methods on the String class to transform names for different purposes. # For instance, you can figure out the name of a table from the name of a class. diff --git a/activesupport/lib/active_support/core_ext/string/inquiry.rb b/activesupport/lib/active_support/core_ext/string/inquiry.rb index c95d83beae..dfa78b2ff8 100644 --- a/activesupport/lib/active_support/core_ext/string/inquiry.rb +++ b/activesupport/lib/active_support/core_ext/string/inquiry.rb @@ -1,4 +1,4 @@ -require "active_support/string_inquirer" +require_relative "../../string_inquirer" class String # Wraps the current string in the <tt>ActiveSupport::StringInquirer</tt> class, diff --git a/activesupport/lib/active_support/core_ext/string/multibyte.rb b/activesupport/lib/active_support/core_ext/string/multibyte.rb index 1c73182259..56a1cede1a 100644 --- a/activesupport/lib/active_support/core_ext/string/multibyte.rb +++ b/activesupport/lib/active_support/core_ext/string/multibyte.rb @@ -1,4 +1,4 @@ -require "active_support/multibyte" +require_relative "../../multibyte" class String # == Multibyte proxy diff --git a/activesupport/lib/active_support/core_ext/string/output_safety.rb b/activesupport/lib/active_support/core_ext/string/output_safety.rb index 94ce3f6a61..607870237b 100644 --- a/activesupport/lib/active_support/core_ext/string/output_safety.rb +++ b/activesupport/lib/active_support/core_ext/string/output_safety.rb @@ -1,6 +1,6 @@ require "erb" -require "active_support/core_ext/kernel/singleton_class" -require "active_support/multibyte/unicode" +require_relative "../kernel/singleton_class" +require_relative "../../multibyte/unicode" class ERB module Util diff --git a/activesupport/lib/active_support/core_ext/string/zones.rb b/activesupport/lib/active_support/core_ext/string/zones.rb index de5a28e4f7..d8d22a6799 100644 --- a/activesupport/lib/active_support/core_ext/string/zones.rb +++ b/activesupport/lib/active_support/core_ext/string/zones.rb @@ -1,5 +1,5 @@ -require "active_support/core_ext/string/conversions" -require "active_support/core_ext/time/zones" +require_relative "conversions" +require_relative "../time/zones" class String # Converts String to a TimeWithZone in the current zone if Time.zone or Time.zone_default diff --git a/activesupport/lib/active_support/core_ext/time.rb b/activesupport/lib/active_support/core_ext/time.rb index b1ae4a45d9..19d4d91dac 100644 --- a/activesupport/lib/active_support/core_ext/time.rb +++ b/activesupport/lib/active_support/core_ext/time.rb @@ -1,5 +1,5 @@ -require "active_support/core_ext/time/acts_like" -require "active_support/core_ext/time/calculations" -require "active_support/core_ext/time/compatibility" -require "active_support/core_ext/time/conversions" -require "active_support/core_ext/time/zones" +require_relative "time/acts_like" +require_relative "time/calculations" +require_relative "time/compatibility" +require_relative "time/conversions" +require_relative "time/zones" diff --git a/activesupport/lib/active_support/core_ext/time/acts_like.rb b/activesupport/lib/active_support/core_ext/time/acts_like.rb index cf4b2539c5..08756a2dae 100644 --- a/activesupport/lib/active_support/core_ext/time/acts_like.rb +++ b/activesupport/lib/active_support/core_ext/time/acts_like.rb @@ -1,4 +1,4 @@ -require "active_support/core_ext/object/acts_like" +require_relative "../object/acts_like" class Time # Duck-types as a Time-like class. See Object#acts_like?. diff --git a/activesupport/lib/active_support/core_ext/time/calculations.rb b/activesupport/lib/active_support/core_ext/time/calculations.rb index 7b7aeef25a..9ba6bf8fa1 100644 --- a/activesupport/lib/active_support/core_ext/time/calculations.rb +++ b/activesupport/lib/active_support/core_ext/time/calculations.rb @@ -1,9 +1,9 @@ -require "active_support/duration" -require "active_support/core_ext/time/conversions" -require "active_support/time_with_zone" -require "active_support/core_ext/time/zones" -require "active_support/core_ext/date_and_time/calculations" -require "active_support/core_ext/date/calculations" +require_relative "../../duration" +require_relative "conversions" +require_relative "../../time_with_zone" +require_relative "zones" +require_relative "../date_and_time/calculations" +require_relative "../date/calculations" class Time include DateAndTime::Calculations @@ -107,21 +107,22 @@ class Time # to the +options+ parameter. The time options (<tt>:hour</tt>, <tt>:min</tt>, # <tt>:sec</tt>, <tt>:usec</tt>, <tt>:nsec</tt>) reset cascadingly, so if only # the hour is passed, then minute, sec, usec and nsec is set to 0. If the hour - # and minute is passed, then sec, usec and nsec is set to 0. The +options+ - # parameter takes a hash with any of these keys: <tt>:year</tt>, <tt>:month</tt>, - # <tt>:day</tt>, <tt>:hour</tt>, <tt>:min</tt>, <tt>:sec</tt>, <tt>:usec</tt> - # <tt>:nsec</tt>. Pass either <tt>:usec</tt> or <tt>:nsec</tt>, not both. + # and minute is passed, then sec, usec and nsec is set to 0. The +options+ parameter + # takes a hash with any of these keys: <tt>:year</tt>, <tt>:month</tt>, <tt>:day</tt>, + # <tt>:hour</tt>, <tt>:min</tt>, <tt>:sec</tt>, <tt>:usec</tt>, <tt>:nsec</tt>, + # <tt>:offset</tt>. Pass either <tt>:usec</tt> or <tt>:nsec</tt>, not both. # # Time.new(2012, 8, 29, 22, 35, 0).change(day: 1) # => Time.new(2012, 8, 1, 22, 35, 0) # Time.new(2012, 8, 29, 22, 35, 0).change(year: 1981, day: 1) # => Time.new(1981, 8, 1, 22, 35, 0) # Time.new(2012, 8, 29, 22, 35, 0).change(year: 1981, hour: 0) # => Time.new(1981, 8, 29, 0, 0, 0) def change(options) - new_year = options.fetch(:year, year) - new_month = options.fetch(:month, month) - new_day = options.fetch(:day, day) - new_hour = options.fetch(:hour, hour) - new_min = options.fetch(:min, options[:hour] ? 0 : min) - new_sec = options.fetch(:sec, (options[:hour] || options[:min]) ? 0 : sec) + new_year = options.fetch(:year, year) + new_month = options.fetch(:month, month) + new_day = options.fetch(:day, day) + new_hour = options.fetch(:hour, hour) + new_min = options.fetch(:min, options[:hour] ? 0 : min) + new_sec = options.fetch(:sec, (options[:hour] || options[:min]) ? 0 : sec) + new_offset = options.fetch(:offset, nil) if new_nsec = options[:nsec] raise ArgumentError, "Can't change both :nsec and :usec at the same time: #{options.inspect}" if options[:usec] @@ -130,13 +131,18 @@ class Time new_usec = options.fetch(:usec, (options[:hour] || options[:min] || options[:sec]) ? 0 : Rational(nsec, 1000)) end - if utc? - ::Time.utc(new_year, new_month, new_day, new_hour, new_min, new_sec, new_usec) + raise ArgumentError, "argument out of range" if new_usec >= 1000000 + + new_sec += Rational(new_usec, 1000000) + + if new_offset + ::Time.new(new_year, new_month, new_day, new_hour, new_min, new_sec, new_offset) + elsif utc? + ::Time.utc(new_year, new_month, new_day, new_hour, new_min, new_sec) elsif zone - ::Time.local(new_year, new_month, new_day, new_hour, new_min, new_sec, new_usec) + ::Time.local(new_year, new_month, new_day, new_hour, new_min, new_sec) else - raise ArgumentError, "argument out of range" if new_usec >= 1000000 - ::Time.new(new_year, new_month, new_day, new_hour, new_min, new_sec + (new_usec.to_r / 1000000), utc_offset) + ::Time.new(new_year, new_month, new_day, new_hour, new_min, new_sec, utc_offset) end end diff --git a/activesupport/lib/active_support/core_ext/time/compatibility.rb b/activesupport/lib/active_support/core_ext/time/compatibility.rb index 45e86b77ce..a554e71b74 100644 --- a/activesupport/lib/active_support/core_ext/time/compatibility.rb +++ b/activesupport/lib/active_support/core_ext/time/compatibility.rb @@ -1,5 +1,5 @@ -require "active_support/core_ext/date_and_time/compatibility" -require "active_support/core_ext/module/remove_method" +require_relative "../date_and_time/compatibility" +require_relative "../module/remove_method" class Time include DateAndTime::Compatibility diff --git a/activesupport/lib/active_support/core_ext/time/conversions.rb b/activesupport/lib/active_support/core_ext/time/conversions.rb index 595bda6b4f..0dbce99ccb 100644 --- a/activesupport/lib/active_support/core_ext/time/conversions.rb +++ b/activesupport/lib/active_support/core_ext/time/conversions.rb @@ -1,5 +1,5 @@ -require "active_support/inflector/methods" -require "active_support/values/time_zone" +require_relative "../../inflector/methods" +require_relative "../../values/time_zone" class Time DATE_FORMATS = { diff --git a/activesupport/lib/active_support/core_ext/time/zones.rb b/activesupport/lib/active_support/core_ext/time/zones.rb index 87b5ad903a..ba8b5b73f8 100644 --- a/activesupport/lib/active_support/core_ext/time/zones.rb +++ b/activesupport/lib/active_support/core_ext/time/zones.rb @@ -1,6 +1,6 @@ -require "active_support/time_with_zone" -require "active_support/core_ext/time/acts_like" -require "active_support/core_ext/date_and_time/zones" +require_relative "../../time_with_zone" +require_relative "acts_like" +require_relative "../date_and_time/zones" class Time include DateAndTime::Zones 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..05b9b5b211 100644 --- a/activesupport/lib/active_support/dependencies.rb +++ b/activesupport/lib/active_support/dependencies.rb @@ -2,24 +2,23 @@ require "set" require "thread" require "concurrent/map" require "pathname" -require "active_support/core_ext/module/aliasing" -require "active_support/core_ext/module/attribute_accessors" -require "active_support/core_ext/module/introspection" -require "active_support/core_ext/module/anonymous" -require "active_support/core_ext/object/blank" -require "active_support/core_ext/kernel/reporting" -require "active_support/core_ext/load_error" -require "active_support/core_ext/name_error" -require "active_support/core_ext/string/starts_ends_with" -require "active_support/dependencies/interlock" -require "active_support/inflector" +require_relative "core_ext/module/aliasing" +require_relative "core_ext/module/attribute_accessors" +require_relative "core_ext/module/introspection" +require_relative "core_ext/module/anonymous" +require_relative "core_ext/object/blank" +require_relative "core_ext/kernel/reporting" +require_relative "core_ext/load_error" +require_relative "core_ext/name_error" +require_relative "core_ext/string/starts_ends_with" +require_relative "dependencies/interlock" +require_relative "inflector" 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/dependencies/autoload.rb b/activesupport/lib/active_support/dependencies/autoload.rb index 13036d521d..9e4d3b783b 100644 --- a/activesupport/lib/active_support/dependencies/autoload.rb +++ b/activesupport/lib/active_support/dependencies/autoload.rb @@ -1,4 +1,4 @@ -require "active_support/inflector/methods" +require_relative "../inflector/methods" module ActiveSupport # Autoload and eager load conveniences for your library. diff --git a/activesupport/lib/active_support/dependencies/interlock.rb b/activesupport/lib/active_support/dependencies/interlock.rb index e4e18439c5..b88cf08dd2 100644 --- a/activesupport/lib/active_support/dependencies/interlock.rb +++ b/activesupport/lib/active_support/dependencies/interlock.rb @@ -1,4 +1,4 @@ -require "active_support/concurrency/share_lock" +require_relative "../concurrency/share_lock" module ActiveSupport #:nodoc: module Dependencies #:nodoc: diff --git a/activesupport/lib/active_support/deprecation.rb b/activesupport/lib/active_support/deprecation.rb index 72c74e966a..eaaf198ccb 100644 --- a/activesupport/lib/active_support/deprecation.rb +++ b/activesupport/lib/active_support/deprecation.rb @@ -12,13 +12,13 @@ module ActiveSupport # a circular require warning for active_support/deprecation.rb. # # So, we define the constant first, and load dependencies later. - require "active_support/deprecation/instance_delegator" - require "active_support/deprecation/behaviors" - require "active_support/deprecation/reporting" - require "active_support/deprecation/constant_accessor" - require "active_support/deprecation/method_wrappers" - require "active_support/deprecation/proxy_wrappers" - require "active_support/core_ext/module/deprecation" + require_relative "deprecation/instance_delegator" + require_relative "deprecation/behaviors" + require_relative "deprecation/reporting" + require_relative "deprecation/constant_accessor" + require_relative "deprecation/method_wrappers" + require_relative "deprecation/proxy_wrappers" + require_relative "core_ext/module/deprecation" include Singleton include InstanceDelegator @@ -30,10 +30,10 @@ module ActiveSupport attr_accessor :deprecation_horizon # It accepts two parameters on initialization. The first is a version of library - # and the second is a library name + # and the second is a library name. # # ActiveSupport::Deprecation.new('2.0', 'MyLibrary') - def initialize(deprecation_horizon = "5.3", gem_name = "Rails") + def initialize(deprecation_horizon = "6.0", gem_name = "Rails") self.gem_name = gem_name self.deprecation_horizon = deprecation_horizon # By default, warnings are not silenced and debugging is off. diff --git a/activesupport/lib/active_support/deprecation/behaviors.rb b/activesupport/lib/active_support/deprecation/behaviors.rb index 1d1354c23e..409f4cf6a6 100644 --- a/activesupport/lib/active_support/deprecation/behaviors.rb +++ b/activesupport/lib/active_support/deprecation/behaviors.rb @@ -1,4 +1,4 @@ -require "active_support/notifications" +require_relative "../notifications" module ActiveSupport # Raised when <tt>ActiveSupport::Deprecation::Behavior#behavior</tt> is set with <tt>:raise</tt>. @@ -9,35 +9,39 @@ module ActiveSupport class Deprecation # Default warning behaviors per Rails.env. DEFAULT_BEHAVIORS = { - raise: ->(message, callstack) { + raise: ->(message, callstack, deprecation_horizon, gem_name) { e = DeprecationException.new(message) e.set_backtrace(callstack.map(&:to_s)) raise e }, - stderr: ->(message, callstack) { + stderr: ->(message, callstack, deprecation_horizon, gem_name) { $stderr.puts(message) $stderr.puts callstack.join("\n ") if debug }, - log: ->(message, callstack) { + log: ->(message, callstack, deprecation_horizon, gem_name) { logger = if defined?(Rails.logger) && Rails.logger Rails.logger else - require "active_support/logger" + require_relative "../logger" ActiveSupport::Logger.new($stderr) end logger.warn message logger.debug callstack.join("\n ") if debug }, - notify: ->(message, callstack) { - ActiveSupport::Notifications.instrument("deprecation.rails", - message: message, callstack: callstack) + notify: ->(message, callstack, deprecation_horizon, gem_name) { + notification_name = "deprecation.#{gem_name.underscore.tr('/', '_')}" + ActiveSupport::Notifications.instrument(notification_name, + message: message, + callstack: callstack, + gem_name: gem_name, + deprecation_horizon: deprecation_horizon) }, - silence: ->(message, callstack) {}, + silence: ->(message, callstack, deprecation_horizon, gem_name) {}, } # Behavior module allows to determine how to display deprecation messages. @@ -83,8 +87,17 @@ module ActiveSupport # # custom stuff # } def behavior=(behavior) - @behavior = Array(behavior).map { |b| DEFAULT_BEHAVIORS[b] || b } + @behavior = Array(behavior).map { |b| DEFAULT_BEHAVIORS[b] || arity_coerce(b) } end + + private + def arity_coerce(behavior) + if behavior.arity == 4 || behavior.arity == -1 + behavior + else + -> message, callstack, _, _ { behavior.call(message, callstack) } + end + end end end end diff --git a/activesupport/lib/active_support/deprecation/constant_accessor.rb b/activesupport/lib/active_support/deprecation/constant_accessor.rb index 2b19de365f..895b23e8ef 100644 --- a/activesupport/lib/active_support/deprecation/constant_accessor.rb +++ b/activesupport/lib/active_support/deprecation/constant_accessor.rb @@ -1,4 +1,4 @@ -require "active_support/inflector/methods" +require_relative "../inflector/methods" module ActiveSupport class Deprecation diff --git a/activesupport/lib/active_support/deprecation/instance_delegator.rb b/activesupport/lib/active_support/deprecation/instance_delegator.rb index 6d390f3b37..23eac0e5d3 100644 --- a/activesupport/lib/active_support/deprecation/instance_delegator.rb +++ b/activesupport/lib/active_support/deprecation/instance_delegator.rb @@ -1,5 +1,5 @@ -require "active_support/core_ext/kernel/singleton_class" -require "active_support/core_ext/module/delegation" +require_relative "../core_ext/kernel/singleton_class" +require_relative "../core_ext/module/delegation" module ActiveSupport class Deprecation diff --git a/activesupport/lib/active_support/deprecation/method_wrappers.rb b/activesupport/lib/active_support/deprecation/method_wrappers.rb index 930d71e8d2..ed41e9aaa4 100644 --- a/activesupport/lib/active_support/deprecation/method_wrappers.rb +++ b/activesupport/lib/active_support/deprecation/method_wrappers.rb @@ -1,5 +1,5 @@ -require "active_support/core_ext/module/aliasing" -require "active_support/core_ext/array/extract_options" +require_relative "../core_ext/module/aliasing" +require_relative "../core_ext/array/extract_options" module ActiveSupport class Deprecation diff --git a/activesupport/lib/active_support/deprecation/proxy_wrappers.rb b/activesupport/lib/active_support/deprecation/proxy_wrappers.rb index ce39e9a232..64aaef3e43 100644 --- a/activesupport/lib/active_support/deprecation/proxy_wrappers.rb +++ b/activesupport/lib/active_support/deprecation/proxy_wrappers.rb @@ -1,5 +1,5 @@ -require "active_support/inflector/methods" -require "active_support/core_ext/regexp" +require_relative "../inflector/methods" +require_relative "../core_ext/regexp" module ActiveSupport class Deprecation diff --git a/activesupport/lib/active_support/deprecation/reporting.rb b/activesupport/lib/active_support/deprecation/reporting.rb index 58c5c50e30..140bdccbb3 100644 --- a/activesupport/lib/active_support/deprecation/reporting.rb +++ b/activesupport/lib/active_support/deprecation/reporting.rb @@ -18,7 +18,7 @@ module ActiveSupport callstack ||= caller_locations(2) deprecation_message(callstack, message).tap do |m| - behavior.each { |b| b.call(m, callstack) } + behavior.each { |b| b.call(m, callstack, deprecation_horizon, gem_name) } end end @@ -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 99080e34a1..0ad591e683 100644 --- a/activesupport/lib/active_support/duration.rb +++ b/activesupport/lib/active_support/duration.rb @@ -1,8 +1,8 @@ -require "active_support/core_ext/array/conversions" -require "active_support/core_ext/module/delegation" -require "active_support/core_ext/object/acts_like" -require "active_support/core_ext/string/filters" -require "active_support/deprecation" +require_relative "core_ext/array/conversions" +require_relative "core_ext/module/delegation" +require_relative "core_ext/object/acts_like" +require_relative "core_ext/string/filters" +require_relative "deprecation" module ActiveSupport # Provides accurate date and time measurements using Date#advance and @@ -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 @@ -305,10 +334,6 @@ module ActiveSupport to_i end - def respond_to_missing?(method, include_private = false) #:nodoc: - @value.respond_to?(method, include_private) - end - # Build ISO 8601 Duration string for this duration. # The +precision+ parameter can be used to limit seconds' precision of duration. def iso8601(precision: nil) @@ -335,8 +360,12 @@ module ActiveSupport end end + def respond_to_missing?(method, _) + value.respond_to?(method) + end + def method_missing(method, *args, &block) - value.send(method, *args, &block) + value.public_send(method, *args, &block) end def raise_type_error(other) diff --git a/activesupport/lib/active_support/duration/iso8601_parser.rb b/activesupport/lib/active_support/duration/iso8601_parser.rb index e96cb8e883..78a00b5e4f 100644 --- a/activesupport/lib/active_support/duration/iso8601_parser.rb +++ b/activesupport/lib/active_support/duration/iso8601_parser.rb @@ -1,5 +1,5 @@ require "strscan" -require "active_support/core_ext/regexp" +require_relative "../core_ext/regexp" module ActiveSupport class Duration diff --git a/activesupport/lib/active_support/duration/iso8601_serializer.rb b/activesupport/lib/active_support/duration/iso8601_serializer.rb index e5d458b3ab..83c3a46692 100644 --- a/activesupport/lib/active_support/duration/iso8601_serializer.rb +++ b/activesupport/lib/active_support/duration/iso8601_serializer.rb @@ -1,5 +1,5 @@ -require "active_support/core_ext/object/blank" -require "active_support/core_ext/hash/transform_values" +require_relative "../core_ext/object/blank" +require_relative "../core_ext/hash/transform_values" module ActiveSupport class Duration @@ -15,12 +15,12 @@ module ActiveSupport parts, sign = normalize return "PT0S".freeze if parts.empty? - output = "P" + output = "P".dup output << "#{parts[:years]}Y" if parts.key?(:years) output << "#{parts[:months]}M" if parts.key?(:months) output << "#{parts[:weeks]}W" if parts.key?(:weeks) output << "#{parts[:days]}D" if parts.key?(:days) - time = "" + time = "".dup time << "#{parts[:hours]}H" if parts.key?(:hours) time << "#{parts[:minutes]}M" if parts.key?(:minutes) if parts.key?(:seconds) diff --git a/activesupport/lib/active_support/execution_wrapper.rb b/activesupport/lib/active_support/execution_wrapper.rb index ca88e7876b..3513e9023c 100644 --- a/activesupport/lib/active_support/execution_wrapper.rb +++ b/activesupport/lib/active_support/execution_wrapper.rb @@ -1,4 +1,4 @@ -require "active_support/callbacks" +require_relative "callbacks" module ActiveSupport class ExecutionWrapper diff --git a/activesupport/lib/active_support/executor.rb b/activesupport/lib/active_support/executor.rb index a6400cae0a..4a618d9fa2 100644 --- a/activesupport/lib/active_support/executor.rb +++ b/activesupport/lib/active_support/executor.rb @@ -1,4 +1,4 @@ -require "active_support/execution_wrapper" +require_relative "execution_wrapper" module ActiveSupport class Executor < ExecutionWrapper diff --git a/activesupport/lib/active_support/file_update_checker.rb b/activesupport/lib/active_support/file_update_checker.rb index 2b5e3c1350..b9d8740569 100644 --- a/activesupport/lib/active_support/file_update_checker.rb +++ b/activesupport/lib/active_support/file_update_checker.rb @@ -1,4 +1,4 @@ -require "active_support/core_ext/time/calculations" +require_relative "core_ext/time/calculations" module ActiveSupport # FileUpdateChecker specifies the API used by Rails to watch files diff --git a/activesupport/lib/active_support/hash_with_indifferent_access.rb b/activesupport/lib/active_support/hash_with_indifferent_access.rb index 1e1d6d3aac..6c98960292 100644 --- a/activesupport/lib/active_support/hash_with_indifferent_access.rb +++ b/activesupport/lib/active_support/hash_with_indifferent_access.rb @@ -1,5 +1,5 @@ -require "active_support/core_ext/hash/keys" -require "active_support/core_ext/hash/reverse_merge" +require_relative "core_ext/hash/keys" +require_relative "core_ext/hash/reverse_merge" module ActiveSupport # Implements a hash where keys <tt>:foo</tt> and <tt>"foo"</tt> are considered @@ -195,6 +195,19 @@ module ActiveSupport indices.collect { |key| self[convert_key(key)] } end + # Returns an array of the values at the specified indices, but also + # raises an exception when one of the keys can't be found. + # + # hash = ActiveSupport::HashWithIndifferentAccess.new + # hash[:a] = 'x' + # hash[:b] = 'y' + # hash.fetch_values('a', 'b') # => ["x", "y"] + # hash.fetch_values('a', 'c') { |key| 'z' } # => ["x", "z"] + # hash.fetch_values('a', 'c') # => KeyError: key not found: "c" + def fetch_values(*indices, &block) + indices.collect { |key| fetch(key, &block) } + end if Hash.method_defined?(:fetch_values) + # Returns a shallow copy of the hash. # # hash = ActiveSupport::HashWithIndifferentAccess.new({ a: { b: 'b' } }) diff --git a/activesupport/lib/active_support/i18n.rb b/activesupport/lib/active_support/i18n.rb index f0408f429c..3f47da3b38 100644 --- a/activesupport/lib/active_support/i18n.rb +++ b/activesupport/lib/active_support/i18n.rb @@ -1,13 +1,13 @@ -require "active_support/core_ext/hash/deep_merge" -require "active_support/core_ext/hash/except" -require "active_support/core_ext/hash/slice" +require_relative "core_ext/hash/deep_merge" +require_relative "core_ext/hash/except" +require_relative "core_ext/hash/slice" begin require "i18n" rescue LoadError => e $stderr.puts "The i18n gem is not available. Please add it to your Gemfile and run bundle install" raise e end -require "active_support/lazy_load_hooks" +require_relative "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..6809a650cf 100644 --- a/activesupport/lib/active_support/i18n_railtie.rb +++ b/activesupport/lib/active_support/i18n_railtie.rb @@ -1,6 +1,6 @@ require "active_support" -require "active_support/file_update_checker" -require "active_support/core_ext/array/wrap" +require_relative "file_update_checker" +require_relative "core_ext/array/wrap" # :enddoc: @@ -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/inflections.rb b/activesupport/lib/active_support/inflections.rb index afa7d1f325..1b94ce9b1b 100644 --- a/activesupport/lib/active_support/inflections.rb +++ b/activesupport/lib/active_support/inflections.rb @@ -1,4 +1,4 @@ -require "active_support/inflector/inflections" +require_relative "inflector/inflections" #-- # Defines the standard inflection rules. These are the starting point for diff --git a/activesupport/lib/active_support/inflector.rb b/activesupport/lib/active_support/inflector.rb index 48631b16a8..a4bd8aaeb4 100644 --- a/activesupport/lib/active_support/inflector.rb +++ b/activesupport/lib/active_support/inflector.rb @@ -1,7 +1,7 @@ # in case active_support/inflector is required without the rest of active_support -require "active_support/inflector/inflections" -require "active_support/inflector/transliterate" -require "active_support/inflector/methods" +require_relative "inflector/inflections" +require_relative "inflector/transliterate" +require_relative "inflector/methods" -require "active_support/inflections" -require "active_support/core_ext/string/inflections" +require_relative "inflections" +require_relative "core_ext/string/inflections" diff --git a/activesupport/lib/active_support/inflector/inflections.rb b/activesupport/lib/active_support/inflector/inflections.rb index c47a2e34e1..2cb7828968 100644 --- a/activesupport/lib/active_support/inflector/inflections.rb +++ b/activesupport/lib/active_support/inflector/inflections.rb @@ -1,7 +1,7 @@ require "concurrent/map" -require "active_support/core_ext/array/prepend_and_append" -require "active_support/core_ext/regexp" -require "active_support/i18n" +require_relative "../core_ext/array/prepend_and_append" +require_relative "../core_ext/regexp" +require_relative "../i18n" module ActiveSupport module Inflector diff --git a/activesupport/lib/active_support/inflector/methods.rb b/activesupport/lib/active_support/inflector/methods.rb index 1b089a7538..ba85d3bb33 100644 --- a/activesupport/lib/active_support/inflector/methods.rb +++ b/activesupport/lib/active_support/inflector/methods.rb @@ -1,5 +1,5 @@ -require "active_support/inflections" -require "active_support/core_ext/regexp" +require_relative "../inflections" +require_relative "../core_ext/regexp" module ActiveSupport # The Inflector transforms words from singular to plural, class names to table @@ -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/inflector/transliterate.rb b/activesupport/lib/active_support/inflector/transliterate.rb index de6dd2720b..0775ee51bc 100644 --- a/activesupport/lib/active_support/inflector/transliterate.rb +++ b/activesupport/lib/active_support/inflector/transliterate.rb @@ -1,5 +1,5 @@ -require "active_support/core_ext/string/multibyte" -require "active_support/i18n" +require_relative "../core_ext/string/multibyte" +require_relative "../i18n" module ActiveSupport module Inflector diff --git a/activesupport/lib/active_support/json.rb b/activesupport/lib/active_support/json.rb index da938d1555..e0a6ab3768 100644 --- a/activesupport/lib/active_support/json.rb +++ b/activesupport/lib/active_support/json.rb @@ -1,2 +1,2 @@ -require "active_support/json/decoding" -require "active_support/json/encoding" +require_relative "json/decoding" +require_relative "json/encoding" diff --git a/activesupport/lib/active_support/json/decoding.rb b/activesupport/lib/active_support/json/decoding.rb index f487fa0c65..c60ebb2795 100644 --- a/activesupport/lib/active_support/json/decoding.rb +++ b/activesupport/lib/active_support/json/decoding.rb @@ -1,5 +1,5 @@ -require "active_support/core_ext/module/attribute_accessors" -require "active_support/core_ext/module/delegation" +require_relative "../core_ext/module/attribute_accessors" +require_relative "../core_ext/module/delegation" require "json" module ActiveSupport diff --git a/activesupport/lib/active_support/json/encoding.rb b/activesupport/lib/active_support/json/encoding.rb index defaf3f395..e7679e4630 100644 --- a/activesupport/lib/active_support/json/encoding.rb +++ b/activesupport/lib/active_support/json/encoding.rb @@ -1,5 +1,5 @@ -require "active_support/core_ext/object/json" -require "active_support/core_ext/module/delegation" +require_relative "../core_ext/object/json" +require_relative "../core_ext/module/delegation" module ActiveSupport class << self diff --git a/activesupport/lib/active_support/log_subscriber.rb b/activesupport/lib/active_support/log_subscriber.rb index e2c4f33565..2b494e6d6c 100644 --- a/activesupport/lib/active_support/log_subscriber.rb +++ b/activesupport/lib/active_support/log_subscriber.rb @@ -1,6 +1,6 @@ -require "active_support/core_ext/module/attribute_accessors" -require "active_support/core_ext/class/attribute" -require "active_support/subscriber" +require_relative "core_ext/module/attribute_accessors" +require_relative "core_ext/class/attribute" +require_relative "subscriber" module ActiveSupport # ActiveSupport::LogSubscriber is an object set to consume @@ -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/log_subscriber/test_helper.rb b/activesupport/lib/active_support/log_subscriber/test_helper.rb index 953ee77c2a..469e6c6bb4 100644 --- a/activesupport/lib/active_support/log_subscriber/test_helper.rb +++ b/activesupport/lib/active_support/log_subscriber/test_helper.rb @@ -1,6 +1,6 @@ -require "active_support/log_subscriber" -require "active_support/logger" -require "active_support/notifications" +require_relative "../log_subscriber" +require_relative "../logger" +require_relative "../notifications" module ActiveSupport class LogSubscriber diff --git a/activesupport/lib/active_support/logger.rb b/activesupport/lib/active_support/logger.rb index ea09d7d2df..677ec8d5bc 100644 --- a/activesupport/lib/active_support/logger.rb +++ b/activesupport/lib/active_support/logger.rb @@ -1,5 +1,5 @@ -require "active_support/logger_silence" -require "active_support/logger_thread_safe_level" +require_relative "logger_silence" +require_relative "logger_thread_safe_level" require "logger" module ActiveSupport diff --git a/activesupport/lib/active_support/logger_silence.rb b/activesupport/lib/active_support/logger_silence.rb index 632994cf50..7f85f54542 100644 --- a/activesupport/lib/active_support/logger_silence.rb +++ b/activesupport/lib/active_support/logger_silence.rb @@ -1,13 +1,12 @@ -require "active_support/concern" -require "active_support/core_ext/module/attribute_accessors" +require_relative "concern" +require_relative "core_ext/module/attribute_accessors" require "concurrent" 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/logger_thread_safe_level.rb b/activesupport/lib/active_support/logger_thread_safe_level.rb index 7fb175dea6..405bb239cf 100644 --- a/activesupport/lib/active_support/logger_thread_safe_level.rb +++ b/activesupport/lib/active_support/logger_thread_safe_level.rb @@ -1,4 +1,4 @@ -require "active_support/concern" +require_relative "concern" module ActiveSupport module LoggerThreadSafeLevel # :nodoc: diff --git a/activesupport/lib/active_support/message_encryptor.rb b/activesupport/lib/active_support/message_encryptor.rb index 24053b4fe5..3aaa1ae02c 100644 --- a/activesupport/lib/active_support/message_encryptor.rb +++ b/activesupport/lib/active_support/message_encryptor.rb @@ -1,7 +1,7 @@ require "openssl" require "base64" -require "active_support/core_ext/array/extract_options" -require "active_support/message_verifier" +require_relative "core_ext/array/extract_options" +require_relative "message_verifier" module ActiveSupport # MessageEncryptor is a simple way to encrypt values which get stored @@ -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 @@ -104,7 +114,7 @@ module ActiveSupport encrypted_data << cipher.final blob = "#{::Base64.strict_encode64 encrypted_data}--#{::Base64.strict_encode64 iv}" - blob << "--#{::Base64.strict_encode64 cipher.auth_tag}" if aead_mode? + blob = "#{blob}--#{::Base64.strict_encode64 cipher.auth_tag}" if aead_mode? blob 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/message_verifier.rb b/activesupport/lib/active_support/message_verifier.rb index 8419e858c6..e71dcd3a5b 100644 --- a/activesupport/lib/active_support/message_verifier.rb +++ b/activesupport/lib/active_support/message_verifier.rb @@ -1,6 +1,6 @@ require "base64" -require "active_support/core_ext/object/blank" -require "active_support/security_utils" +require_relative "core_ext/object/blank" +require_relative "security_utils" module ActiveSupport # +MessageVerifier+ makes it easy to generate and verify messages which are diff --git a/activesupport/lib/active_support/multibyte/chars.rb b/activesupport/lib/active_support/multibyte/chars.rb index 8c58466556..e4c544d1fa 100644 --- a/activesupport/lib/active_support/multibyte/chars.rb +++ b/activesupport/lib/active_support/multibyte/chars.rb @@ -1,8 +1,8 @@ -require "active_support/json" -require "active_support/core_ext/string/access" -require "active_support/core_ext/string/behavior" -require "active_support/core_ext/module/delegation" -require "active_support/core_ext/regexp" +require_relative "../json" +require_relative "../core_ext/string/access" +require_relative "../core_ext/string/behavior" +require_relative "../core_ext/module/delegation" +require_relative "../core_ext/regexp" module ActiveSupport #:nodoc: module Multibyte #:nodoc: 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/notifications.rb b/activesupport/lib/active_support/notifications.rb index 2df819e554..10e40e04f9 100644 --- a/activesupport/lib/active_support/notifications.rb +++ b/activesupport/lib/active_support/notifications.rb @@ -1,6 +1,6 @@ -require "active_support/notifications/instrumenter" -require "active_support/notifications/fanout" -require "active_support/per_thread_registry" +require_relative "notifications/instrumenter" +require_relative "notifications/fanout" +require_relative "per_thread_registry" module ActiveSupport # = Notifications @@ -64,6 +64,8 @@ module ActiveSupport # If an exception happens during that particular instrumentation the payload will # have a key <tt>:exception</tt> with an array of two elements as value: a string with # the name of the exception class, and the exception message. + # The <tt>:exception_object</tt> key of the payload will have the exception + # itself as the value. # # As the previous example depicts, the class <tt>ActiveSupport::Notifications::Event</tt> # is able to take the arguments as they come and provide an object-oriented 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_converter.rb b/activesupport/lib/active_support/number_helper/number_converter.rb index ce363287cf..f3fed6e88b 100644 --- a/activesupport/lib/active_support/number_helper/number_converter.rb +++ b/activesupport/lib/active_support/number_helper/number_converter.rb @@ -1,8 +1,8 @@ -require "active_support/core_ext/big_decimal/conversions" -require "active_support/core_ext/object/blank" -require "active_support/core_ext/hash/keys" -require "active_support/i18n" -require "active_support/core_ext/class/attribute" +require_relative "../core_ext/big_decimal/conversions" +require_relative "../core_ext/object/blank" +require_relative "../core_ext/hash/keys" +require_relative "../i18n" +require_relative "../core_ext/class/attribute" module ActiveSupport module NumberHelper diff --git a/activesupport/lib/active_support/number_helper/number_to_currency_converter.rb b/activesupport/lib/active_support/number_helper/number_to_currency_converter.rb index 0f9dce722f..7bd2d4869a 100644 --- a/activesupport/lib/active_support/number_helper/number_to_currency_converter.rb +++ b/activesupport/lib/active_support/number_helper/number_to_currency_converter.rb @@ -1,4 +1,4 @@ -require "active_support/core_ext/numeric/inquiry" +require_relative "../core_ext/numeric/inquiry" module ActiveSupport module NumberHelper 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_phone_converter.rb b/activesupport/lib/active_support/number_helper/number_to_phone_converter.rb index 1de9f50f34..3546ee5c2a 100644 --- a/activesupport/lib/active_support/number_helper/number_to_phone_converter.rb +++ b/activesupport/lib/active_support/number_helper/number_to_phone_converter.rb @@ -2,7 +2,7 @@ module ActiveSupport module NumberHelper class NumberToPhoneConverter < NumberConverter #:nodoc: def convert - str = country_code(opts[:country_code]) + str = country_code(opts[:country_code]).dup str << convert_to_phone_number(number.to_s.strip) str << phone_ext(opts[:extension]) 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/option_merger.rb b/activesupport/lib/active_support/option_merger.rb index 0f2caa98f2..9edb0e6cd8 100644 --- a/activesupport/lib/active_support/option_merger.rb +++ b/activesupport/lib/active_support/option_merger.rb @@ -1,4 +1,4 @@ -require "active_support/core_ext/hash/deep_merge" +require_relative "core_ext/hash/deep_merge" module ActiveSupport class OptionMerger #:nodoc: diff --git a/activesupport/lib/active_support/ordered_options.rb b/activesupport/lib/active_support/ordered_options.rb index 04d6dfaf9c..c0f39a392b 100644 --- a/activesupport/lib/active_support/ordered_options.rb +++ b/activesupport/lib/active_support/ordered_options.rb @@ -1,4 +1,4 @@ -require "active_support/core_ext/object/blank" +require_relative "core_ext/object/blank" module ActiveSupport # Usually key value pairs are handled something like this: diff --git a/activesupport/lib/active_support/per_thread_registry.rb b/activesupport/lib/active_support/per_thread_registry.rb index 02431704d3..1e0d8345a0 100644 --- a/activesupport/lib/active_support/per_thread_registry.rb +++ b/activesupport/lib/active_support/per_thread_registry.rb @@ -1,4 +1,4 @@ -require "active_support/core_ext/module/delegation" +require_relative "core_ext/module/delegation" module ActiveSupport # NOTE: This approach has been deprecated for end-user code in favor of {thread_mattr_accessor}[rdoc-ref:Module#thread_mattr_accessor] and friends. diff --git a/activesupport/lib/active_support/rails.rb b/activesupport/lib/active_support/rails.rb index f6b018f0d3..656af6dee9 100644 --- a/activesupport/lib/active_support/rails.rb +++ b/activesupport/lib/active_support/rails.rb @@ -9,25 +9,25 @@ # Rails and can change anytime. # Defines Object#blank? and Object#present?. -require "active_support/core_ext/object/blank" +require_relative "core_ext/object/blank" # Rails own autoload, eager_load, etc. -require "active_support/dependencies/autoload" +require_relative "dependencies/autoload" # Support for ClassMethods and the included macro. -require "active_support/concern" +require_relative "concern" # Defines Class#class_attribute. -require "active_support/core_ext/class/attribute" +require_relative "core_ext/class/attribute" # Defines Module#delegate. -require "active_support/core_ext/module/delegation" +require_relative "core_ext/module/delegation" # Defines ActiveSupport::Deprecation. -require "active_support/deprecation" +require_relative "deprecation" # Defines Regexp#match?. # # This should be removed when Rails needs Ruby 2.4 or later, and the require # added where other Regexp extensions are being used (easy to grep). -require "active_support/core_ext/regexp" +require_relative "core_ext/regexp" diff --git a/activesupport/lib/active_support/railtie.rb b/activesupport/lib/active_support/railtie.rb index b875875afe..3377748263 100644 --- a/activesupport/lib/active_support/railtie.rb +++ b/activesupport/lib/active_support/railtie.rb @@ -1,5 +1,5 @@ require "active_support" -require "active_support/i18n_railtie" +require_relative "i18n_railtie" module ActiveSupport class Railtie < Rails::Railtie # :nodoc: @@ -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 @@ -21,21 +34,14 @@ module ActiveSupport rescue TZInfo::DataSourceNotFound => e 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 + require_relative "core_ext/time/zones" + Time.zone_default = Time.find_zone!(app.config.time_zone) end # Sets the default week start # If assigned value is not a valid day symbol (e.g. :sunday, :monday, ...), an exception will be raised. initializer "active_support.initialize_beginning_of_week" do |app| - require "active_support/core_ext/date/calculations" + require_relative "core_ext/date/calculations" beginning_of_week_default = Date.find_beginning_of_week!(app.config.beginning_of_week) Date.beginning_of_week_default = beginning_of_week_default diff --git a/activesupport/lib/active_support/reloader.rb b/activesupport/lib/active_support/reloader.rb index 121c621751..a87c82e68a 100644 --- a/activesupport/lib/active_support/reloader.rb +++ b/activesupport/lib/active_support/reloader.rb @@ -1,4 +1,4 @@ -require "active_support/execution_wrapper" +require_relative "execution_wrapper" module ActiveSupport #-- @@ -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..06f6a71de4 100644 --- a/activesupport/lib/active_support/rescuable.rb +++ b/activesupport/lib/active_support/rescuable.rb @@ -1,6 +1,6 @@ -require "active_support/concern" -require "active_support/core_ext/class/attribute" -require "active_support/core_ext/string/inflections" +require_relative "concern" +require_relative "core_ext/class/attribute" +require_relative "core_ext/string/inflections" module ActiveSupport # Rescuable module adds support for easier exception handling. @@ -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/subscriber.rb b/activesupport/lib/active_support/subscriber.rb index 2924139755..3a54482c75 100644 --- a/activesupport/lib/active_support/subscriber.rb +++ b/activesupport/lib/active_support/subscriber.rb @@ -1,5 +1,5 @@ -require "active_support/per_thread_registry" -require "active_support/notifications" +require_relative "per_thread_registry" +require_relative "notifications" module ActiveSupport # ActiveSupport::Subscriber is an object set to consume diff --git a/activesupport/lib/active_support/tagged_logging.rb b/activesupport/lib/active_support/tagged_logging.rb index ad134c49b6..90d096c397 100644 --- a/activesupport/lib/active_support/tagged_logging.rb +++ b/activesupport/lib/active_support/tagged_logging.rb @@ -1,7 +1,7 @@ -require "active_support/core_ext/module/delegation" -require "active_support/core_ext/object/blank" +require_relative "core_ext/module/delegation" +require_relative "core_ext/object/blank" require "logger" -require "active_support/logger" +require_relative "logger" module ActiveSupport # Wraps any standard Logger object to provide tagging capabilities. diff --git a/activesupport/lib/active_support/test_case.rb b/activesupport/lib/active_support/test_case.rb index 3de4ccc1da..f2940a176f 100644 --- a/activesupport/lib/active_support/test_case.rb +++ b/activesupport/lib/active_support/test_case.rb @@ -1,15 +1,15 @@ gem "minitest" # make sure we get the gem, not stdlib require "minitest" -require "active_support/testing/tagged_logging" -require "active_support/testing/setup_and_teardown" -require "active_support/testing/assertions" -require "active_support/testing/deprecation" -require "active_support/testing/declarative" -require "active_support/testing/isolation" -require "active_support/testing/constant_lookup" -require "active_support/testing/time_helpers" -require "active_support/testing/file_fixtures" -require "active_support/core_ext/kernel/reporting" +require_relative "testing/tagged_logging" +require_relative "testing/setup_and_teardown" +require_relative "testing/assertions" +require_relative "testing/deprecation" +require_relative "testing/declarative" +require_relative "testing/isolation" +require_relative "testing/constant_lookup" +require_relative "testing/time_helpers" +require_relative "testing/file_fixtures" +require_relative "core_ext/kernel/reporting" module ActiveSupport class TestCase < ::Minitest::Test 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/lib/active_support/testing/constant_lookup.rb b/activesupport/lib/active_support/testing/constant_lookup.rb index 647395d2b3..c0b58aba96 100644 --- a/activesupport/lib/active_support/testing/constant_lookup.rb +++ b/activesupport/lib/active_support/testing/constant_lookup.rb @@ -1,5 +1,5 @@ -require "active_support/concern" -require "active_support/inflector" +require_relative "../concern" +require_relative "../inflector" module ActiveSupport module Testing diff --git a/activesupport/lib/active_support/testing/deprecation.rb b/activesupport/lib/active_support/testing/deprecation.rb index 58911570e8..6ee49530f0 100644 --- a/activesupport/lib/active_support/testing/deprecation.rb +++ b/activesupport/lib/active_support/testing/deprecation.rb @@ -1,5 +1,5 @@ -require "active_support/deprecation" -require "active_support/core_ext/regexp" +require_relative "../deprecation" +require_relative "../core_ext/regexp" module ActiveSupport module Testing diff --git a/activesupport/lib/active_support/testing/setup_and_teardown.rb b/activesupport/lib/active_support/testing/setup_and_teardown.rb index 358c79c321..c39f3bac2b 100644 --- a/activesupport/lib/active_support/testing/setup_and_teardown.rb +++ b/activesupport/lib/active_support/testing/setup_and_teardown.rb @@ -1,5 +1,5 @@ -require "active_support/concern" -require "active_support/callbacks" +require_relative "../concern" +require_relative "../callbacks" module ActiveSupport module Testing diff --git a/activesupport/lib/active_support/testing/time_helpers.rb b/activesupport/lib/active_support/testing/time_helpers.rb index 07c9be0604..4db7065ddf 100644 --- a/activesupport/lib/active_support/testing/time_helpers.rb +++ b/activesupport/lib/active_support/testing/time_helpers.rb @@ -1,4 +1,5 @@ -require "active_support/core_ext/string/strip" # for strip_heredoc +require_relative "../core_ext/string/strip" # for strip_heredoc +require_relative "../core_ext/time/calculations" require "concurrent/map" module ActiveSupport diff --git a/activesupport/lib/active_support/time.rb b/activesupport/lib/active_support/time.rb index 7658228ca6..bc5f0177e2 100644 --- a/activesupport/lib/active_support/time.rb +++ b/activesupport/lib/active_support/time.rb @@ -7,12 +7,12 @@ end require "date" require "time" -require "active_support/core_ext/time" -require "active_support/core_ext/date" -require "active_support/core_ext/date_time" +require_relative "core_ext/time" +require_relative "core_ext/date" +require_relative "core_ext/date_time" -require "active_support/core_ext/integer/time" -require "active_support/core_ext/numeric/time" +require_relative "core_ext/integer/time" +require_relative "core_ext/numeric/time" -require "active_support/core_ext/string/conversions" -require "active_support/core_ext/string/zones" +require_relative "core_ext/string/conversions" +require_relative "core_ext/string/zones" diff --git a/activesupport/lib/active_support/time_with_zone.rb b/activesupport/lib/active_support/time_with_zone.rb index b0dd6b7e8c..67d4a8683b 100644 --- a/activesupport/lib/active_support/time_with_zone.rb +++ b/activesupport/lib/active_support/time_with_zone.rb @@ -1,7 +1,7 @@ -require "active_support/duration" -require "active_support/values/time_zone" -require "active_support/core_ext/object/acts_like" -require "active_support/core_ext/date_and_time/compatibility" +require_relative "duration" +require_relative "values/time_zone" +require_relative "core_ext/object/acts_like" +require_relative "core_ext/date_and_time/compatibility" module ActiveSupport # A Time-like class that can represent a time in any time zone. Necessary @@ -330,6 +330,42 @@ module ActiveSupport since(-other) end + # Returns a new +ActiveSupport::TimeWithZone+ where one or more of the elements have + # been changed according to the +options+ parameter. The time options (<tt>:hour</tt>, + # <tt>:min</tt>, <tt>:sec</tt>, <tt>:usec</tt>, <tt>:nsec</tt>) reset cascadingly, + # so if only the hour is passed, then minute, sec, usec and nsec is set to 0. If the + # hour and minute is passed, then sec, usec and nsec is set to 0. The +options+ + # parameter takes a hash with any of these keys: <tt>:year</tt>, <tt>:month</tt>, + # <tt>:day</tt>, <tt>:hour</tt>, <tt>:min</tt>, <tt>:sec</tt>, <tt>:usec</tt>, + # <tt>:nsec</tt>, <tt>:offset</tt>, <tt>:zone</tt>. Pass either <tt>:usec</tt> + # or <tt>:nsec</tt>, not both. Similarly, pass either <tt>:zone</tt> or + # <tt>:offset</tt>, not both. + # + # t = Time.zone.now # => Fri, 14 Apr 2017 11:45:15 EST -05:00 + # t.change(year: 2020) # => Tue, 14 Apr 2020 11:45:15 EST -05:00 + # t.change(hour: 12) # => Fri, 14 Apr 2017 12:00:00 EST -05:00 + # t.change(min: 30) # => Fri, 14 Apr 2017 11:30:00 EST -05:00 + # t.change(offset: "-10:00") # => Fri, 14 Apr 2017 11:45:15 HST -10:00 + # t.change(zone: "Hawaii") # => Fri, 14 Apr 2017 11:45:15 HST -10:00 + def change(options) + if options[:zone] && options[:offset] + raise ArgumentError, "Can't change both :offset and :zone at the same time: #{options.inspect}" + end + + new_time = time.change(options) + + if options[:zone] + new_zone = ::Time.find_zone(options[:zone]) + elsif options[:offset] + new_zone = ::Time.find_zone(new_time.utc_offset) + end + + new_zone ||= time_zone + periods = new_zone.periods_for_local(new_time) + + self.class.new(nil, new_zone, new_time, periods.include?(period) ? period : nil) + end + # Uses Date to provide precise Time calculations for years, months, and days # according to the proleptic Gregorian calendar. The result is returned as a # new TimeWithZone object. diff --git a/activesupport/lib/active_support/values/time_zone.rb b/activesupport/lib/active_support/values/time_zone.rb index dd69383f5f..890a2af51c 100644 --- a/activesupport/lib/active_support/values/time_zone.rb +++ b/activesupport/lib/active_support/values/time_zone.rb @@ -1,12 +1,12 @@ require "tzinfo" require "concurrent/map" -require "active_support/core_ext/object/blank" +require_relative "../core_ext/object/blank" module ActiveSupport # The TimeZone class serves as a wrapper around TZInfo::Timezone instances. # It allows us to do the following: # - # * Limit the set of zones provided by TZInfo to a meaningful subset of 146 + # * Limit the set of zones provided by TZInfo to a meaningful subset of 134 # zones. # * Retrieve and display zones with a friendlier name # (e.g., "Eastern Time (US & Canada)" instead of "America/New_York"). @@ -59,6 +59,7 @@ module ActiveSupport "Buenos Aires" => "America/Argentina/Buenos_Aires", "Montevideo" => "America/Montevideo", "Georgetown" => "America/Guyana", + "Puerto Rico" => "America/Puerto_Rico", "Greenland" => "America/Godthab", "Mid-Atlantic" => "Atlantic/South_Georgia", "Azores" => "Atlantic/Azores", diff --git a/activesupport/lib/active_support/xml_mini.rb b/activesupport/lib/active_support/xml_mini.rb index 782fb41288..14725ffd64 100644 --- a/activesupport/lib/active_support/xml_mini.rb +++ b/activesupport/lib/active_support/xml_mini.rb @@ -1,9 +1,9 @@ require "time" require "base64" require "bigdecimal" -require "active_support/core_ext/module/delegation" -require "active_support/core_ext/string/inflections" -require "active_support/core_ext/date_time/calculations" +require_relative "core_ext/module/delegation" +require_relative "core_ext/string/inflections" +require_relative "core_ext/date_time/calculations" module ActiveSupport # = XmlMini @@ -197,7 +197,7 @@ module ActiveSupport if name.is_a?(Module) name else - require "active_support/xml_mini/#{name.downcase}" + require_relative "xml_mini/#{name.downcase}" ActiveSupport.const_get("XmlMini_#{name}") end end diff --git a/activesupport/lib/active_support/xml_mini/jdom.rb b/activesupport/lib/active_support/xml_mini/jdom.rb index a7939b3185..331d9960e7 100644 --- a/activesupport/lib/active_support/xml_mini/jdom.rb +++ b/activesupport/lib/active_support/xml_mini/jdom.rb @@ -3,7 +3,7 @@ raise "JRuby is required to use the JDOM backend for XmlMini" unless RUBY_PLATFO require "jruby" include Java -require "active_support/core_ext/object/blank" +require_relative "../core_ext/object/blank" java_import javax.xml.parsers.DocumentBuilder unless defined? DocumentBuilder java_import javax.xml.parsers.DocumentBuilderFactory unless defined? DocumentBuilderFactory diff --git a/activesupport/lib/active_support/xml_mini/libxml.rb b/activesupport/lib/active_support/xml_mini/libxml.rb index cde2967132..d7097dcc64 100644 --- a/activesupport/lib/active_support/xml_mini/libxml.rb +++ b/activesupport/lib/active_support/xml_mini/libxml.rb @@ -1,5 +1,5 @@ require "libxml" -require "active_support/core_ext/object/blank" +require_relative "../core_ext/object/blank" require "stringio" module ActiveSupport @@ -14,11 +14,9 @@ module ActiveSupport data = StringIO.new(data || "") end - char = data.getc - if char.nil? + if data.eof? {} else - data.ungetc(char) LibXML::XML::Parser.io(data).parse.to_hash end end @@ -55,7 +53,7 @@ module LibXML #:nodoc: if c.element? c.to_hash(node_hash) elsif c.text? || c.cdata? - node_hash[CONTENT_ROOT] ||= "" + node_hash[CONTENT_ROOT] ||= "".dup node_hash[CONTENT_ROOT] << c.content end end diff --git a/activesupport/lib/active_support/xml_mini/libxmlsax.rb b/activesupport/lib/active_support/xml_mini/libxmlsax.rb index 8a43b05b17..0cfa2b2cd3 100644 --- a/activesupport/lib/active_support/xml_mini/libxmlsax.rb +++ b/activesupport/lib/active_support/xml_mini/libxmlsax.rb @@ -1,5 +1,5 @@ require "libxml" -require "active_support/core_ext/object/blank" +require_relative "../core_ext/object/blank" require "stringio" module ActiveSupport @@ -21,7 +21,7 @@ module ActiveSupport end def on_start_document - @hash = { CONTENT_KEY => "" } + @hash = { CONTENT_KEY => "".dup } @hash_stack = [@hash] end @@ -31,7 +31,7 @@ module ActiveSupport end def on_start_element(name, attrs = {}) - new_hash = { CONTENT_KEY => "" }.merge!(attrs) + new_hash = { CONTENT_KEY => "".dup }.merge!(attrs) new_hash[HASH_SIZE_KEY] = new_hash.size + 1 case current_hash[name] @@ -65,12 +65,9 @@ module ActiveSupport data = StringIO.new(data || "") end - char = data.getc - if char.nil? + if data.eof? {} else - data.ungetc(char) - LibXML::XML::Error.set_handler(&LibXML::XML::Error::QUIET_HANDLER) parser = LibXML::XML::SaxParser.io(data) document = document_class.new diff --git a/activesupport/lib/active_support/xml_mini/nokogiri.rb b/activesupport/lib/active_support/xml_mini/nokogiri.rb index 4c2be3f3ec..57d8cec0c6 100644 --- a/activesupport/lib/active_support/xml_mini/nokogiri.rb +++ b/activesupport/lib/active_support/xml_mini/nokogiri.rb @@ -4,7 +4,7 @@ rescue LoadError => e $stderr.puts "You don't have nokogiri installed in your application. Please add it to your Gemfile and run bundle install" raise e end -require "active_support/core_ext/object/blank" +require_relative "../core_ext/object/blank" require "stringio" module ActiveSupport @@ -19,11 +19,9 @@ module ActiveSupport data = StringIO.new(data || "") end - char = data.getc - if char.nil? + if data.eof? {} else - data.ungetc(char) doc = Nokogiri::XML(data) raise doc.errors.first if doc.errors.length > 0 doc.to_hash @@ -59,7 +57,7 @@ module ActiveSupport if c.element? c.to_hash(node_hash) elsif c.text? || c.cdata? - node_hash[CONTENT_ROOT] ||= "" + node_hash[CONTENT_ROOT] ||= "".dup node_hash[CONTENT_ROOT] << c.content end end diff --git a/activesupport/lib/active_support/xml_mini/nokogirisax.rb b/activesupport/lib/active_support/xml_mini/nokogirisax.rb index 7388bea5d8..8fc1dfa727 100644 --- a/activesupport/lib/active_support/xml_mini/nokogirisax.rb +++ b/activesupport/lib/active_support/xml_mini/nokogirisax.rb @@ -4,7 +4,7 @@ rescue LoadError => e $stderr.puts "You don't have nokogiri installed in your application. Please add it to your Gemfile and run bundle install" raise e end -require "active_support/core_ext/object/blank" +require_relative "../core_ext/object/blank" require "stringio" module ActiveSupport @@ -37,7 +37,7 @@ module ActiveSupport end def start_element(name, attrs = []) - new_hash = { CONTENT_KEY => "" }.merge!(Hash[attrs]) + new_hash = { CONTENT_KEY => "".dup }.merge!(Hash[attrs]) new_hash[HASH_SIZE_KEY] = new_hash.size + 1 case current_hash[name] @@ -71,11 +71,9 @@ module ActiveSupport data = StringIO.new(data || "") end - char = data.getc - if char.nil? + if data.eof? {} else - data.ungetc(char) document = document_class.new parser = Nokogiri::XML::SAX::Parser.new(document) parser.parse(data) diff --git a/activesupport/lib/active_support/xml_mini/rexml.rb b/activesupport/lib/active_support/xml_mini/rexml.rb index 03fa910fa5..04d12c6109 100644 --- a/activesupport/lib/active_support/xml_mini/rexml.rb +++ b/activesupport/lib/active_support/xml_mini/rexml.rb @@ -1,5 +1,5 @@ -require "active_support/core_ext/kernel/reporting" -require "active_support/core_ext/object/blank" +require_relative "../core_ext/kernel/reporting" +require_relative "../core_ext/object/blank" require "stringio" module ActiveSupport @@ -74,7 +74,7 @@ module ActiveSupport hash else # must use value to prevent double-escaping - texts = "" + texts = "".dup element.texts.each { |t| texts << t.value } merge!(hash, CONTENT_KEY, texts) end 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..4631e6aee8 --- /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".dup + @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 = "".dup + 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..6adef2916d --- /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".dup.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".dup.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".dup.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..ea39d7a299 --- /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".dup + 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".dup + 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/array/conversions_test.rb b/activesupport/test/core_ext/array/conversions_test.rb index 29e661a99b..bbe81ae70c 100644 --- a/activesupport/test/core_ext/array/conversions_test.rb +++ b/activesupport/test/core_ext/array/conversions_test.rb @@ -58,7 +58,7 @@ class ToSentenceTest < ActiveSupport::TestCase ["one", "two"].to_sentence(passing: "invalid option") end - assert_equal exception.message, "Unknown key: :passing. Valid keys are: :words_connector, :two_words_connector, :last_word_connector, :locale" + assert_equal "Unknown key: :passing. Valid keys are: :words_connector, :two_words_connector, :last_word_connector, :locale", exception.message end def test_always_returns_string 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 36f0ee22b8..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 @@ -166,6 +188,9 @@ class DateTimeExtCalculationsTest < ActiveSupport::TestCase assert_equal DateTime.civil(2005, 2, 22, 16, 45), DateTime.civil(2005, 2, 22, 15, 15, 10).change(hour: 16, min: 45) assert_equal DateTime.civil(2005, 2, 22, 15, 45), DateTime.civil(2005, 2, 22, 15, 15, 10).change(min: 45) + # datetime with non-zero offset + assert_equal DateTime.civil(2005, 2, 22, 15, 15, 10, Rational(-5, 24)), DateTime.civil(2005, 2, 22, 15, 15, 10, 0).change(offset: Rational(-5, 24)) + # datetime with fractions of a second assert_equal DateTime.civil(2005, 2, 1, 15, 15, 10.7), DateTime.civil(2005, 2, 22, 15, 15, 10.7).change(day: 1) assert_equal DateTime.civil(2005, 1, 2, 11, 22, Rational(33000008, 1000000)), DateTime.civil(2005, 1, 2, 11, 22, 33).change(usec: 8) 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/enumerable_test.rb b/activesupport/test/core_ext/enumerable_test.rb index 4f1ab993b8..0b345ecf0f 100644 --- a/activesupport/test/core_ext/enumerable_test.rb +++ b/activesupport/test/core_ext/enumerable_test.rb @@ -171,10 +171,8 @@ class EnumerableTests < ActiveSupport::TestCase assert_equal({ 5 => Payment.new(5), 15 => Payment.new(15), 10 => Payment.new(10) }, payments.index_by(&:price)) assert_equal Enumerator, payments.index_by.class - if Enumerator.method_defined? :size - assert_nil payments.index_by.size - assert_equal 42, (1..42).index_by.size - end + assert_nil payments.index_by.size + assert_equal 42, (1..42).index_by.size assert_equal({ 5 => Payment.new(5), 15 => Payment.new(15), 10 => Payment.new(10) }, payments.index_by.each(&:price)) 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 a17438bf4d..68bd5233bf 100644 --- a/activesupport/test/core_ext/module_test.rb +++ b/activesupport/test/core_ext/module_test.rb @@ -70,7 +70,23 @@ Product = Struct.new(:name) do end end +module ExtraMissing + def method_missing(sym, *args) + if sym == :extra_missing + 42 + else + super + end + end + + def respond_to_missing?(sym, priv = false) + sym == :extra_missing || super + end +end + DecoratedTester = Struct.new(:client) do + include ExtraMissing + delegate_missing_to :client end @@ -332,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 @@ -348,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 @@ -356,8 +372,62 @@ class ModuleTest < ActiveSupport::TestCase assert_match(/undefined method `my_fake_method' for/, e.message) end + 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) + + assert DecoratedTester.new(@david).respond_to?(:name, true) + assert_not DecoratedTester.new(@david).respond_to?(:private_name, true) + assert_not DecoratedTester.new(@david).respond_to?(:my_fake_method, true) + end + + def test_delegate_missing_to_respects_superclass_missing + assert_equal 42, DecoratedTester.new(@david).extra_missing + + assert_respond_to DecoratedTester.new(@david), :extra_missing + end + def test_delegate_with_case event = Event.new(Tester.new) assert_equal 1, event.foo end + + def test_private_delegate + location = Class.new do + def initialize(place) + @place = place + end + + private(*delegate(:street, :city, to: :@place)) + end + + place = location.new(Somewhere.new("Such street", "Sad city")) + + assert_not place.respond_to?(:street) + assert_not place.respond_to?(:city) + + assert place.respond_to?(:street, true) # Asking for private method + assert place.respond_to?(:city, true) + end + + def test_private_delegate_prefixed + location = Class.new do + def initialize(place) + @place = place + end + + private(*delegate(:street, :city, to: :@place, prefix: :the)) + end + + place = location.new(Somewhere.new("Such street", "Sad city")) + + assert_not place.respond_to?(:street) + assert_not place.respond_to?(:city) + + assert_not place.respond_to?(:the_street) + assert place.respond_to?(:the_street, true) + assert_not place.respond_to?(:the_city) + assert place.respond_to?(:the_city, true) + end end diff --git a/activesupport/test/core_ext/string_ext_test.rb b/activesupport/test/core_ext/string_ext_test.rb index a98951e889..daa8c3f2ef 100644 --- a/activesupport/test/core_ext/string_ext_test.rb +++ b/activesupport/test/core_ext/string_ext_test.rb @@ -233,7 +233,7 @@ class StringInflectionsTest < ActiveSupport::TestCase def test_string_squish original = %{\u205f\u3000 A string surrounded by various unicode spaces, - with tabs(\t\t), newlines(\n\n), unicode nextlines(\u0085\u0085) and many spaces( ). \u00a0\u2007} + with tabs(\t\t), newlines(\n\n), unicode nextlines(\u0085\u0085) and many spaces( ). \u00a0\u2007}.dup expected = "A string surrounded by various unicode spaces, " \ "with tabs( ), newlines( ), unicode nextlines( ) and many spaces( )." @@ -303,8 +303,8 @@ class StringInflectionsTest < ActiveSupport::TestCase end def test_truncate_multibyte - assert_equal "\354\225\204\353\246\254\353\236\221 \354\225\204\353\246\254 ...".force_encoding(Encoding::UTF_8), - "\354\225\204\353\246\254\353\236\221 \354\225\204\353\246\254 \354\225\204\353\235\274\353\246\254\354\230\244".force_encoding(Encoding::UTF_8).truncate(10) + assert_equal "\354\225\204\353\246\254\353\236\221 \354\225\204\353\246\254 ...".dup.force_encoding(Encoding::UTF_8), + "\354\225\204\353\246\254\353\236\221 \354\225\204\353\246\254 \354\225\204\353\235\274\353\246\254\354\230\244".dup.force_encoding(Encoding::UTF_8).truncate(10) end def test_truncate_should_not_be_html_safe @@ -325,7 +325,7 @@ class StringInflectionsTest < ActiveSupport::TestCase end def test_remove! - original = "This is a very good day to die" + original = "This is a very good day to die".dup assert_equal "This is a good day to die", original.remove!(" very") assert_equal "This is a good day to die", original assert_equal "This is a good day", original.remove!(" to ", /die/) @@ -658,7 +658,7 @@ end class OutputSafetyTest < ActiveSupport::TestCase def setup - @string = "hello" + @string = "hello".dup @object = Class.new(Object) do def to_s "other" @@ -734,7 +734,7 @@ class OutputSafetyTest < ActiveSupport::TestCase end test "Concatting safe onto unsafe yields unsafe" do - @other_string = "other" + @other_string = "other".dup string = @string.html_safe @other_string.concat(string) @@ -757,7 +757,7 @@ class OutputSafetyTest < ActiveSupport::TestCase end test "Concatting safe onto unsafe with << yields unsafe" do - @other_string = "other" + @other_string = "other".dup string = @string.html_safe @other_string << string @@ -813,7 +813,7 @@ class OutputSafetyTest < ActiveSupport::TestCase test "Concatting an integer to safe always yields safe" do string = @string.html_safe string = string.concat(13) - assert_equal "hello".concat(13), string + assert_equal "hello".dup.concat(13), string assert string.html_safe? end @@ -868,7 +868,8 @@ end class StringIndentTest < ActiveSupport::TestCase test "does not indent strings that only contain newlines (edge cases)" do - ["", "\n", "\n" * 7].each do |str| + ["", "\n", "\n" * 7].each do |string| + str = string.dup assert_nil str.indent!(8) assert_equal str, str.indent(8) assert_equal str, str.indent(1, "\t") diff --git a/activesupport/test/core_ext/time_ext_test.rb b/activesupport/test/core_ext/time_ext_test.rb index bd644c8457..625a5bffb8 100644 --- a/activesupport/test/core_ext/time_ext_test.rb +++ b/activesupport/test/core_ext/time_ext_test.rb @@ -433,6 +433,13 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase assert_raise(ArgumentError) { Time.new(2005, 2, 22, 15, 15, 45, "-08:00").change(nsec: 1000000000) } end + def test_change_offset + assert_equal Time.new(2006, 2, 22, 15, 15, 10, "-08:00"), Time.new(2006, 2, 22, 15, 15, 10, "+01:00").change(offset: "-08:00") + assert_equal Time.new(2006, 2, 22, 15, 15, 10, -28800), Time.new(2006, 2, 22, 15, 15, 10, 3600).change(offset: -28800) + assert_raise(ArgumentError) { Time.new(2005, 2, 22, 15, 15, 45, "+01:00").change(usec: 1000000, offset: "-08:00") } + assert_raise(ArgumentError) { Time.new(2005, 2, 22, 15, 15, 45, "+01:00").change(nsec: 1000000000, offset: -28800) } + end + def test_advance assert_equal Time.local(2006, 2, 28, 15, 15, 10), Time.local(2005, 2, 28, 15, 15, 10).advance(years: 1) assert_equal Time.local(2005, 6, 28, 15, 15, 10), Time.local(2005, 2, 28, 15, 15, 10).advance(months: 4) diff --git a/activesupport/test/core_ext/time_with_zone_test.rb b/activesupport/test/core_ext/time_with_zone_test.rb index c3afe68378..70ae793cda 100644 --- a/activesupport/test/core_ext/time_with_zone_test.rb +++ b/activesupport/test/core_ext/time_with_zone_test.rb @@ -625,6 +625,12 @@ class TimeWithZoneTest < ActiveSupport::TestCase assert_equal "Fri, 31 Dec 1999 06:00:00 EST -05:00", @twz.change(hour: 6).inspect assert_equal "Fri, 31 Dec 1999 19:15:00 EST -05:00", @twz.change(min: 15).inspect assert_equal "Fri, 31 Dec 1999 19:00:30 EST -05:00", @twz.change(sec: 30).inspect + assert_equal "Fri, 31 Dec 1999 19:00:00 HST -10:00", @twz.change(offset: "-10:00").inspect + assert_equal "Fri, 31 Dec 1999 19:00:00 HST -10:00", @twz.change(offset: -36000).inspect + assert_equal "Fri, 31 Dec 1999 19:00:00 HST -10:00", @twz.change(zone: "Hawaii").inspect + assert_equal "Fri, 31 Dec 1999 19:00:00 HST -10:00", @twz.change(zone: -10).inspect + assert_equal "Fri, 31 Dec 1999 19:00:00 HST -10:00", @twz.change(zone: -36000).inspect + assert_equal "Fri, 31 Dec 1999 19:00:00 HST -10:00", @twz.change(zone: "Pacific/Honolulu").inspect end def test_change_at_dst_boundary 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/deprecation_test.rb b/activesupport/test/deprecation_test.rb index 36d1ef0849..257cb50fb2 100644 --- a/activesupport/test/deprecation_test.rb +++ b/activesupport/test/deprecation_test.rb @@ -100,16 +100,18 @@ class DeprecationTest < ActiveSupport::TestCase end def test_several_behaviors - @a, @b = nil, nil + @a, @b, @c = nil, nil, nil ActiveSupport::Deprecation.behavior = [ - Proc.new { |msg, callstack| @a = msg }, - Proc.new { |msg, callstack| @b = msg } + lambda { |msg, callstack, horizon, gem| @a = msg }, + lambda { |msg, callstack| @b = msg }, + lambda { |*args| @c = args }, ] @dtc.partially assert_match(/foo=nil/, @a) assert_match(/foo=nil/, @b) + assert_equal 4, @c.size end def test_raise_behaviour @@ -119,7 +121,7 @@ class DeprecationTest < ActiveSupport::TestCase callstack = caller_locations e = assert_raise ActiveSupport::DeprecationException do - ActiveSupport::Deprecation.behavior.first.call(message, callstack) + ActiveSupport::Deprecation.behavior.first.call(message, callstack, "horizon", "gem") end assert_equal message, e.message assert_equal callstack.map(&:to_s), e.backtrace.map(&:to_s) @@ -130,7 +132,7 @@ class DeprecationTest < ActiveSupport::TestCase behavior = ActiveSupport::Deprecation.behavior.first content = capture(:stderr) { - assert_nil behavior.call("Some error!", ["call stack!"]) + assert_nil behavior.call("Some error!", ["call stack!"], "horizon", "gem") } assert_match(/Some error!/, content) assert_match(/call stack!/, content) @@ -152,11 +154,32 @@ class DeprecationTest < ActiveSupport::TestCase behavior = ActiveSupport::Deprecation.behavior.first stderr_output = capture(:stderr) { - assert_nil behavior.call("Some error!", ["call stack!"]) + assert_nil behavior.call("Some error!", ["call stack!"], "horizon", "gem") } assert stderr_output.empty? end + def test_default_notify_behavior + ActiveSupport::Deprecation.behavior = :notify + behavior = ActiveSupport::Deprecation.behavior.first + + begin + events = [] + ActiveSupport::Notifications.subscribe("deprecation.my_gem_custom") { |_, **args| + events << args + } + + assert_nil behavior.call("Some error!", ["call stack!"], "horizon", "MyGem::Custom") + assert_equal 1, events.size + assert_equal "Some error!", events.first[:message] + assert_equal ["call stack!"], events.first[:callstack] + assert_equal "horizon", events.first[:deprecation_horizon] + assert_equal "MyGem::Custom", events.first[:gem_name] + ensure + ActiveSupport::Notifications.unsubscribe("deprecation.my_gem_custom") + end + end + def test_deprecated_instance_variable_proxy assert_not_deprecated { @dtc.request.size } diff --git a/activesupport/test/hash_with_indifferent_access_test.rb b/activesupport/test/hash_with_indifferent_access_test.rb index ee862320e8..d68add46cd 100644 --- a/activesupport/test/hash_with_indifferent_access_test.rb +++ b/activesupport/test/hash_with_indifferent_access_test.rb @@ -166,6 +166,18 @@ class HashWithIndifferentAccessTest < ActiveSupport::TestCase assert_equal [1, 2], @mixed.values_at(:a, :b) end + def test_indifferent_fetch_values + skip unless Hash.method_defined?(:fetch_values) + + @mixed = @mixed.with_indifferent_access + + assert_equal [1, 2], @mixed.fetch_values("a", "b") + assert_equal [1, 2], @mixed.fetch_values(:a, :b) + assert_equal [1, 2], @mixed.fetch_values(:a, "b") + assert_equal [1, "c"], @mixed.fetch_values(:a, :c) { |key| key } + assert_raise(KeyError) { @mixed.fetch_values(:a, :c) } + end + def test_indifferent_reading hash = HashWithIndifferentAccess.new hash["a"] = 1 @@ -675,7 +687,6 @@ class HashWithIndifferentAccessTest < ActiveSupport::TestCase assert_includes yaml_output, "hash" end end - HashWithIndifferentAccess = ActiveSupport::HashWithIndifferentAccess def test_should_use_default_proc_for_unknown_key hash_wia = HashWithIndifferentAccess.new { 1 + 2 } 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/logger_test.rb b/activesupport/test/logger_test.rb index 3f04783401..1bc443ae65 100644 --- a/activesupport/test/logger_test.rb +++ b/activesupport/test/logger_test.rb @@ -37,7 +37,7 @@ class LoggerTest < ActiveSupport::TestCase logger = Logger.new f logger.level = Logger::DEBUG - str = "\x80" + str = "\x80".dup str.force_encoding("ASCII-8BIT") logger.add Logger::DEBUG, str @@ -55,7 +55,7 @@ class LoggerTest < ActiveSupport::TestCase logger = Logger.new f logger.level = Logger::DEBUG - str = "\x80" + str = "\x80".dup str.force_encoding("ASCII-8BIT") logger.add Logger::DEBUG, str 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/multibyte_chars_test.rb b/activesupport/test/multibyte_chars_test.rb index d80d340986..16a7d488c7 100644 --- a/activesupport/test/multibyte_chars_test.rb +++ b/activesupport/test/multibyte_chars_test.rb @@ -51,7 +51,7 @@ class MultibyteCharsTest < ActiveSupport::TestCase end def test_forwarded_method_with_non_string_result_should_be_returned_verbatim - str = "" + str = "".dup str.singleton_class.class_eval { def __method_for_multibyte_testing_with_integer_result; 1; end } @chars.wrapped_string.singleton_class.class_eval { def __method_for_multibyte_testing_with_integer_result; 1; end } @@ -59,14 +59,14 @@ class MultibyteCharsTest < ActiveSupport::TestCase end def test_should_concatenate - mb_a = "a".mb_chars - mb_b = "b".mb_chars + mb_a = "a".dup.mb_chars + mb_b = "b".dup.mb_chars assert_equal "ab", mb_a + "b" assert_equal "ab", "a" + mb_b assert_equal "ab", mb_a + mb_b assert_equal "ab", mb_a << "b" - assert_equal "ab", "a" << mb_b + assert_equal "ab", "a".dup << mb_b assert_equal "abb", mb_a << mb_b end @@ -78,7 +78,7 @@ class MultibyteCharsTest < ActiveSupport::TestCase def test_concatenation_should_return_a_proxy_class_instance assert_equal ActiveSupport::Multibyte.proxy_class, ("a".mb_chars + "b").class - assert_equal ActiveSupport::Multibyte.proxy_class, ("a".mb_chars << "b").class + assert_equal ActiveSupport::Multibyte.proxy_class, ("a".dup.mb_chars << "b").class end def test_ascii_strings_are_treated_at_utf8_strings @@ -88,8 +88,8 @@ class MultibyteCharsTest < ActiveSupport::TestCase def test_concatenate_should_return_proxy_instance assert(("a".mb_chars + "b").kind_of?(@proxy_class)) assert(("a".mb_chars + "b".mb_chars).kind_of?(@proxy_class)) - assert(("a".mb_chars << "b").kind_of?(@proxy_class)) - assert(("a".mb_chars << "b".mb_chars).kind_of?(@proxy_class)) + assert(("a".dup.mb_chars << "b").kind_of?(@proxy_class)) + assert(("a".dup.mb_chars << "b".mb_chars).kind_of?(@proxy_class)) end def test_should_return_string_as_json @@ -115,12 +115,12 @@ class MultibyteCharsUTF8BehaviourTest < ActiveSupport::TestCase %w{capitalize downcase lstrip reverse rstrip swapcase upcase}.each do |method| class_eval(<<-EOTESTS, __FILE__, __LINE__ + 1) def test_#{method}_bang_should_return_self_when_modifying_wrapped_string - chars = ' él piDió Un bUen café ' + chars = ' él piDió Un bUen café '.dup assert_equal chars.object_id, chars.send("#{method}!").object_id end def test_#{method}_bang_should_change_wrapped_string - original = ' él piDió Un bUen café ' + original = ' él piDió Un bUen café '.dup proxy = chars(original.dup) proxy.send("#{method}!") assert_not_equal original, proxy.to_s @@ -133,7 +133,7 @@ class MultibyteCharsUTF8BehaviourTest < ActiveSupport::TestCase end def test_tidy_bytes_bang_should_change_wrapped_string - original = " Un bUen café \x92" + original = " Un bUen café \x92".dup proxy = chars(original.dup) proxy.tidy_bytes! assert_not_equal original, proxy.to_s @@ -150,7 +150,7 @@ class MultibyteCharsUTF8BehaviourTest < ActiveSupport::TestCase end def test_string_methods_are_chainable - assert chars("").insert(0, "").kind_of?(ActiveSupport::Multibyte.proxy_class) + assert chars("".dup).insert(0, "").kind_of?(ActiveSupport::Multibyte.proxy_class) assert chars("").rjust(1).kind_of?(ActiveSupport::Multibyte.proxy_class) assert chars("").ljust(1).kind_of?(ActiveSupport::Multibyte.proxy_class) assert chars("").center(1).kind_of?(ActiveSupport::Multibyte.proxy_class) @@ -195,7 +195,7 @@ class MultibyteCharsUTF8BehaviourTest < ActiveSupport::TestCase end def test_should_use_character_offsets_for_insert_offsets - assert_equal "", "".mb_chars.insert(0, "") + assert_equal "", "".dup.mb_chars.insert(0, "") assert_equal "こわにちわ", @chars.insert(1, "わ") assert_equal "こわわわにちわ", @chars.insert(2, "わわ") assert_equal "わこわわわにちわ", @chars.insert(0, "わ") @@ -418,13 +418,13 @@ class MultibyteCharsUTF8BehaviourTest < ActiveSupport::TestCase end def test_slice_bang_removes_the_slice_from_the_receiver - chars = "úüù".mb_chars + chars = "úüù".dup.mb_chars chars.slice!(0, 2) assert_equal "ù", chars end def test_slice_bang_returns_nil_and_does_not_modify_receiver_if_out_of_bounds - string = "úüù" + string = "úüù".dup chars = string.mb_chars assert_nil chars.slice!(4, 5) assert_equal "úüù", chars diff --git a/activesupport/test/multibyte_test_helpers.rb b/activesupport/test/multibyte_test_helpers.rb index a70516bb08..e1e0ae86d9 100644 --- a/activesupport/test/multibyte_test_helpers.rb +++ b/activesupport/test/multibyte_test_helpers.rb @@ -23,7 +23,7 @@ module MultibyteTestHelpers UNICODE_STRING = "こにちわ".freeze ASCII_STRING = "ohayo".freeze - BYTE_STRING = "\270\236\010\210\245".force_encoding("ASCII-8BIT").freeze + BYTE_STRING = "\270\236\010\210\245".dup.force_encoding("ASCII-8BIT").freeze def chars(str) ActiveSupport::Multibyte::Chars.new(str) 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 diff --git a/activesupport/test/xml_mini/xml_mini_engine_test.rb b/activesupport/test/xml_mini/xml_mini_engine_test.rb index 5be9084c9d..244e0b0d3a 100644 --- a/activesupport/test/xml_mini/xml_mini_engine_test.rb +++ b/activesupport/test/xml_mini/xml_mini_engine_test.rb @@ -75,6 +75,11 @@ class XMLMiniEngineTest < ActiveSupport::TestCase assert_equal({}, ActiveSupport::XmlMini.parse("")) end + def test_parse_from_frozen_string + xml_string = "<root/>".freeze + assert_equal({ "root" => {} }, ActiveSupport::XmlMini.parse(xml_string)) + end + def test_array_type_makes_an_array assert_equal_rexml(<<-eoxml) <blog> |