diff options
Diffstat (limited to 'activesupport')
68 files changed, 1024 insertions, 197 deletions
diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md index 6272dd3471..597e7b8929 100644 --- a/activesupport/CHANGELOG.md +++ b/activesupport/CHANGELOG.md @@ -1,3 +1,138 @@ +* Calling test methods with `with_info_handler` method to allow minitest-hooks + plugin to work. + + *Mauri Mustonen* + +* The Zeitwerk compatibility interface for `ActiveSupport::Dependencies` no + longer implements `autoloaded_constants` or `autoloaded?` (undocumented, + anyway). Experience shows introspection does not have many use cases, and + troubleshooting is done by logging. With this design trade-off we are able + to use even less memory in all environments. + + *Xavier Noria* + +* Depends on Zeitwerk 2, which stores less metadata if reloading is disabled + and hence uses less memory when `config.cache_classes` is `true`, a standard + setup in production. + + *Xavier Noria* + +* In `:zeitwerk` mode, eager load directories in engines and applications only + if present in their respective `config.eager_load_paths`. + + A common use case for this is adding `lib` to `config.autoload_paths`, but + not to `config.eager_load_paths`. In that configuration, for example, files + in the `lib` directory should not be eager loaded. + + *Xavier Noria* + +* Fix bug in Range comparisons when comparing to an excluded-end Range + + Before: + + (1..10).cover?(1...11) # => false + + After: + + (1..10).cover?(1...11) # => true + + With the same change for `Range#include?` and `Range#===`. + + *Owen Stephens* + +* Use weak references in descendants tracker to allow anonymous subclasses to + be garbage collected. + + *Edgars Beigarts* + +* Update `ActiveSupport::Notifications::Instrumenter#instrument` to make + passing a block optional. This will let users use + `ActiveSupport::Notifications` messaging features outside of + instrumentation. + + *Ali Ibrahim* + +* Fix `Time#advance` to work with dates before 1001-03-07 + + Before: + + Time.utc(1001, 3, 6).advance(years: -1) # => 1000-03-05 00:00:00 UTC + + After + + Time.utc(1001, 3, 6).advance(years: -1) # => 1000-03-06 00:00:00 UTC + + Note that this doesn't affect `DateTime#advance` as that doesn't use a proleptic calendar. + + *Andrew White* + +* In Zeitwerk mode, engines are now managed by the `main` autoloader. Engines may reference application constants, if the application is reloaded and we do not reload engines, they won't use the reloaded application code. + + *Xavier Noria* + +* Add support for supplying `locale` to `transliterate` and `parameterize`. + + I18n.backend.store_translations(:de, i18n: { transliterate: { rule: { "ü" => "ue" } } }) + + ActiveSupport::Inflector.transliterate("ü", locale: :de) # => "ue" + "Fünf autos".parameterize(locale: :de) # => "fuenf-autos" + ActiveSupport::Inflector.parameterize("Fünf autos", locale: :de) # => "fuenf-autos" + + *Kaan Ozkan*, *Sharang Dashputre* + +* Allow `Array#excluding` and `Enumerable#excluding` to deal with a passed array gracefully. + + [ 1, 2, 3, 4, 5 ].excluding([4, 5]) # => [ 1, 2, 3 ] + + *DHH* + +* Renamed `Array#without` and `Enumerable#without` to `Array#excluding` and `Enumerable#excluding`, to create parity with + `Array#including` and `Enumerable#including`. Retained the old names as aliases. + + *DHH* + +* Added `Array#including` and `Enumerable#including` to conveniently enlarge a collection with more members using a method rather than an operator: + + [ 1, 2, 3 ].including(4, 5) # => [ 1, 2, 3, 4, 5 ] + post.authors.including(Current.person) # => All the authors plus the current person! + + *DHH* + + +## Rails 6.0.0.beta3 (March 11, 2019) ## + +* No changes. + + +## Rails 6.0.0.beta2 (February 25, 2019) ## + +* New autoloading based on [Zeitwerk](https://github.com/fxn/zeitwerk). + + *Xavier Noria* + +* Revise `ActiveSupport::Notifications.unsubscribe` to correctly handle Regex or other multiple-pattern subscribers. + + *Zach Kemp* + +* Add `before_reset` callback to `CurrentAttributes` and define `after_reset` as an alias of `resets` for symmetry. + + *Rosa Gutierrez* + +* Remove the `` Kernel#` `` override that suppresses ENOENT and accidentally returns nil on Unix systems. + + *Akinori Musha* + +* Add `ActiveSupport::HashWithIndifferentAccess#assoc`. + + `assoc` can now be called with either a string or a symbol. + + *Stefan Schüßler* + +* Add `Hash#deep_transform_values`, and `Hash#deep_transform_values!`. + + *Guillermo Iguaran* + + ## Rails 6.0.0.beta1 (January 18, 2019) ## * Remove deprecated `Module#reachable?` method. @@ -205,7 +340,7 @@ *Kasper Timm Hansen* -* Fix bug where `ActiveSupport::Timezone.all` would fail when tzinfo data for +* Fix bug where `ActiveSupport::TimeZone.all` would fail when tzinfo data for any timezone defined in `ActiveSupport::TimeZone::MAPPING` is missing. *Dominik Sander* diff --git a/activesupport/README.rdoc b/activesupport/README.rdoc index c770324be8..d8d25d86c8 100644 --- a/activesupport/README.rdoc +++ b/activesupport/README.rdoc @@ -5,6 +5,7 @@ extensions that were found useful for the Rails framework. These additions reside in this package so they can be loaded as needed in Ruby projects outside of Rails. +You can read more about the extensions in the {Active Support Core Extensions}[https://edgeguides.rubyonrails.org/active_support_core_extensions.html] guide. == Download and installation @@ -28,7 +29,7 @@ Active Support is released under the MIT license: API documentation is at: -* http://api.rubyonrails.org +* https://api.rubyonrails.org Bug reports for the Ruby on Rails project can be filed here: diff --git a/activesupport/activesupport.gemspec b/activesupport/activesupport.gemspec index bdd7bc70a0..dd2340cdd3 100644 --- a/activesupport/activesupport.gemspec +++ b/activesupport/activesupport.gemspec @@ -15,7 +15,7 @@ Gem::Specification.new do |s| s.author = "David Heinemeier Hansson" s.email = "david@loudthinking.com" - s.homepage = "http://rubyonrails.org" + s.homepage = "https://rubyonrails.org" s.files = Dir["CHANGELOG.md", "MIT-LICENSE", "README.rdoc", "lib/**/*"] s.require_path = "lib" @@ -30,8 +30,9 @@ Gem::Specification.new do |s| # NOTE: Please read our dependency guidelines before updating versions: # https://edgeguides.rubyonrails.org/security.html#dependency-management-and-cves - s.add_dependency "i18n", ">= 0.7", "< 2" - s.add_dependency "tzinfo", "~> 1.1" - s.add_dependency "minitest", "~> 5.1" + s.add_dependency "i18n", ">= 0.7", "< 2" + s.add_dependency "tzinfo", "~> 1.1" + s.add_dependency "minitest", "~> 5.1" s.add_dependency "concurrent-ruby", "~> 1.0", ">= 1.0.2" + s.add_dependency "zeitwerk", "~> 2.1", ">= 2.1.2" end diff --git a/activesupport/lib/active_support/backtrace_cleaner.rb b/activesupport/lib/active_support/backtrace_cleaner.rb index 62973eca58..02cbfbaee6 100644 --- a/activesupport/lib/active_support/backtrace_cleaner.rb +++ b/activesupport/lib/active_support/backtrace_cleaner.rb @@ -122,7 +122,11 @@ module ActiveSupport end def noise(backtrace) - backtrace - silence(backtrace) + backtrace.select do |line| + @silencers.any? do |s| + s.call(line) + end + end end end end diff --git a/activesupport/lib/active_support/cache.rb b/activesupport/lib/active_support/cache.rb index 30a69c550b..e055135bb4 100644 --- a/activesupport/lib/active_support/cache.rb +++ b/activesupport/lib/active_support/cache.rb @@ -492,7 +492,7 @@ module ActiveSupport # # Options are passed to the underlying cache implementation. # - # All implementations may not support this method. + # Some implementations may not support this method. def delete_matched(matcher, options = nil) raise NotImplementedError.new("#{self.class.name} does not support delete_matched") end @@ -501,7 +501,7 @@ module ActiveSupport # # Options are passed to the underlying cache implementation. # - # All implementations may not support this method. + # Some implementations may not support this method. def increment(name, amount = 1, options = nil) raise NotImplementedError.new("#{self.class.name} does not support increment") end @@ -510,7 +510,7 @@ module ActiveSupport # # Options are passed to the underlying cache implementation. # - # All implementations may not support this method. + # Some implementations may not support this method. def decrement(name, amount = 1, options = nil) raise NotImplementedError.new("#{self.class.name} does not support decrement") end @@ -519,7 +519,7 @@ module ActiveSupport # # Options are passed to the underlying cache implementation. # - # All implementations may not support this method. + # Some implementations may not support this method. def cleanup(options = nil) raise NotImplementedError.new("#{self.class.name} does not support cleanup") end @@ -529,7 +529,7 @@ module ActiveSupport # # The options hash is passed to the underlying cache implementation. # - # All implementations may not support this method. + # Some implementations may not support this method. def clear(options = nil) raise NotImplementedError.new("#{self.class.name} does not support clear") end diff --git a/activesupport/lib/active_support/cache/file_store.rb b/activesupport/lib/active_support/cache/file_store.rb index de1fb1886c..f43894a1ea 100644 --- a/activesupport/lib/active_support/cache/file_store.rb +++ b/activesupport/lib/active_support/cache/file_store.rb @@ -18,7 +18,6 @@ module ActiveSupport DIR_FORMATTER = "%03X" FILENAME_MAX_SIZE = 228 # max filename size on file system is 255, minus room for timestamp and random characters appended by Tempfile (used by atomic write) FILEPATH_MAX_SIZE = 900 # max is 1024, plus some room - EXCLUDED_DIRS = [".", ".."].freeze GITKEEP_FILES = [".gitkeep", ".keep"].freeze def initialize(cache_path, options = nil) @@ -35,7 +34,7 @@ module ActiveSupport # 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(options = nil) - root_dirs = exclude_from(cache_path, EXCLUDED_DIRS + GITKEEP_FILES) + root_dirs = (Dir.children(cache_path) - GITKEEP_FILES) FileUtils.rm_r(root_dirs.collect { |f| File.join(cache_path, f) }) rescue Errno::ENOENT end @@ -154,7 +153,7 @@ module ActiveSupport # Delete empty directories in the cache. def delete_empty_directories(dir) return if File.realpath(dir) == File.realpath(cache_path) - if exclude_from(dir, EXCLUDED_DIRS).empty? + if Dir.children(dir).empty? Dir.delete(dir) rescue nil delete_empty_directories(File.dirname(dir)) end @@ -167,8 +166,7 @@ module ActiveSupport def search_dir(dir, &callback) return if !File.exist?(dir) - Dir.foreach(dir) do |d| - next if EXCLUDED_DIRS.include?(d) + Dir.each_child(dir) do |d| name = File.join(dir, d) if File.directory?(name) search_dir(name, &callback) @@ -193,11 +191,6 @@ module ActiveSupport end end end - - # Exclude entries from source directory - def exclude_from(source, excludes) - Dir.entries(source).reject { |f| excludes.include?(f) } - end end end end diff --git a/activesupport/lib/active_support/cache/memory_store.rb b/activesupport/lib/active_support/cache/memory_store.rb index 106b616529..629eb2dd70 100644 --- a/activesupport/lib/active_support/cache/memory_store.rb +++ b/activesupport/lib/active_support/cache/memory_store.rb @@ -62,13 +62,13 @@ module ActiveSupport return if pruning? @pruning = true begin - start_time = Time.now + start_time = Concurrent.monotonic_time cleanup instrument(:prune, target_size, from: @cache_size) do keys = synchronize { @key_access.keys.sort { |a, b| @key_access[a].to_f <=> @key_access[b].to_f } } keys.each do |key| delete_entry(key, options) - return if @cache_size <= target_size || (max_time && Time.now - start_time > max_time) + return if @cache_size <= target_size || (max_time && Concurrent.monotonic_time - start_time > max_time) end end ensure diff --git a/activesupport/lib/active_support/cache/redis_cache_store.rb b/activesupport/lib/active_support/cache/redis_cache_store.rb index 9a55e49e27..87f9aa5346 100644 --- a/activesupport/lib/active_support/cache/redis_cache_store.rb +++ b/activesupport/lib/active_support/cache/redis_cache_store.rb @@ -361,6 +361,7 @@ module ActiveSupport def read_multi_mget(*names) options = names.extract_options! options = merged_options(options) + return {} if names == [] keys = names.map { |name| normalize_key(name, options) } diff --git a/activesupport/lib/active_support/concern.rb b/activesupport/lib/active_support/concern.rb index 5d356a0ab6..708c445031 100644 --- a/activesupport/lib/active_support/concern.rb +++ b/activesupport/lib/active_support/concern.rb @@ -110,7 +110,7 @@ module ActiveSupport base.instance_variable_set(:@_dependencies, []) end - def append_features(base) + def append_features(base) #:nodoc: if base.instance_variable_defined?(:@_dependencies) base.instance_variable_get(:@_dependencies) << self false @@ -123,6 +123,9 @@ module ActiveSupport end end + # Evaluate given block in context of base class, + # so that you can write class macros here. + # When you define more than one +included+ block, it raises an exception. def included(base = nil, &block) if base.nil? if instance_variable_defined?(:@_included_block) @@ -137,6 +140,26 @@ module ActiveSupport end end + # Define class methods from given block. + # You can define private class methods as well. + # + # module Example + # extend ActiveSupport::Concern + # + # class_methods do + # def foo; puts 'foo'; end + # + # private + # def bar; puts 'bar'; end + # end + # end + # + # class Buzz + # include Example + # end + # + # Buzz.foo # => "foo" + # Buzz.bar # => private method 'bar' called for Buzz:Class(NoMethodError) def class_methods(&class_methods_module_definition) mod = const_defined?(:ClassMethods, false) ? const_get(:ClassMethods) : diff --git a/activesupport/lib/active_support/configurable.rb b/activesupport/lib/active_support/configurable.rb index 9acf674c40..71c23dae9b 100644 --- a/activesupport/lib/active_support/configurable.rb +++ b/activesupport/lib/active_support/configurable.rb @@ -67,8 +67,8 @@ module ActiveSupport # end # # => NameError: invalid config attribute name # - # To opt out of the instance writer method, pass <tt>instance_writer: false</tt>. - # To opt out of the instance reader method, pass <tt>instance_reader: false</tt>. + # To omit the instance writer method, pass <tt>instance_writer: false</tt>. + # To omit the instance reader method, pass <tt>instance_reader: false</tt>. # # class User # include ActiveSupport::Configurable @@ -81,7 +81,7 @@ module ActiveSupport # User.new.allowed_access = true # => NoMethodError # User.new.allowed_access # => NoMethodError # - # Or pass <tt>instance_accessor: false</tt>, to opt out both instance methods. + # Or pass <tt>instance_accessor: false</tt>, to omit both instance methods. # # class User # include ActiveSupport::Configurable diff --git a/activesupport/lib/active_support/core_ext/array/access.rb b/activesupport/lib/active_support/core_ext/array/access.rb index b7ff7a3907..ea01e5891c 100644 --- a/activesupport/lib/active_support/core_ext/array/access.rb +++ b/activesupport/lib/active_support/core_ext/array/access.rb @@ -29,16 +29,28 @@ class Array end end - # Returns a copy of the Array without the specified elements. + # Returns a new array that includes the passed elements. # - # people = ["David", "Rafael", "Aaron", "Todd"] - # people.without "Aaron", "Todd" - # # => ["David", "Rafael"] + # [ 1, 2, 3 ].including(4, 5) # => [ 1, 2, 3, 4, 5 ] + # [ [ 0, 1 ] ].including([ [ 1, 0 ] ]) # => [ [ 0, 1 ], [ 1, 0 ] ] + def including(*elements) + self + elements.flatten(1) + end + + # Returns a copy of the Array excluding the specified elements. + # + # ["David", "Rafael", "Aaron", "Todd"].excluding("Aaron", "Todd") # => ["David", "Rafael"] + # [ [ 0, 1 ], [ 1, 0 ] ].excluding([ [ 1, 0 ] ]) # => [ [ 0, 1 ] ] # - # Note: This is an optimization of <tt>Enumerable#without</tt> that uses <tt>Array#-</tt> + # Note: This is an optimization of <tt>Enumerable#excluding</tt> that uses <tt>Array#-</tt> # instead of <tt>Array#reject</tt> for performance reasons. + def excluding(*elements) + self - elements.flatten(1) + end + + # Alias for #excluding. def without(*elements) - self - elements + excluding(*elements) end # Equal to <tt>self[1]</tt>. diff --git a/activesupport/lib/active_support/core_ext/class/attribute.rb b/activesupport/lib/active_support/core_ext/class/attribute.rb index fa33ff945f..255cbee55c 100644 --- a/activesupport/lib/active_support/core_ext/class/attribute.rb +++ b/activesupport/lib/active_support/core_ext/class/attribute.rb @@ -84,16 +84,17 @@ class Class # 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_predicate = options.fetch(:instance_predicate, true) - default_value = options.fetch(:default, nil) - + def class_attribute( + *attrs, + instance_accessor: true, + instance_reader: instance_accessor, + instance_writer: instance_accessor, + instance_predicate: true, + default: nil + ) attrs.each do |name| singleton_class.silence_redefinition_of_method(name) - define_singleton_method(name) { nil } + define_singleton_method(name) { default } singleton_class.silence_redefinition_of_method("#{name}?") define_singleton_method("#{name}?") { !!public_send(name) } if instance_predicate @@ -102,9 +103,7 @@ class Class singleton_class.silence_redefinition_of_method("#{name}=") define_singleton_method("#{name}=") do |val| - singleton_class.class_eval do - redefine_method(name) { val } - end + redefine_singleton_method(name) { val } if singleton_class? class_eval do @@ -137,10 +136,6 @@ class Class instance_variable_set ivar, val end end - - unless default_value.nil? - self.send("#{name}=", default_value) - end end end end diff --git a/activesupport/lib/active_support/core_ext/enumerable.rb b/activesupport/lib/active_support/core_ext/enumerable.rb index d87d63f287..4675c41936 100644 --- a/activesupport/lib/active_support/core_ext/enumerable.rb +++ b/activesupport/lib/active_support/core_ext/enumerable.rb @@ -97,23 +97,43 @@ module Enumerable end end + # Returns a new array that includes the passed elements. + # + # [ 1, 2, 3 ].including(4, 5) + # # => [ 1, 2, 3, 4, 5 ] + # + # ["David", "Rafael"].including %w[ Aaron Todd ] + # # => ["David", "Rafael", "Aaron", "Todd"] + def including(*elements) + to_a.including(*elements) + end + # The negative of the <tt>Enumerable#include?</tt>. Returns +true+ if the # collection does not include the object. def exclude?(object) !include?(object) end - # Returns a copy of the enumerable without the specified elements. + # Returns a copy of the enumerable excluding the specified elements. + # + # ["David", "Rafael", "Aaron", "Todd"].excluding "Aaron", "Todd" + # # => ["David", "Rafael"] # - # ["David", "Rafael", "Aaron", "Todd"].without "Aaron", "Todd" + # ["David", "Rafael", "Aaron", "Todd"].excluding %w[ Aaron Todd ] # # => ["David", "Rafael"] # - # {foo: 1, bar: 2, baz: 3}.without :bar + # {foo: 1, bar: 2, baz: 3}.excluding :bar # # => {foo: 1, baz: 3} - def without(*elements) + def excluding(*elements) + elements.flatten!(1) reject { |element| elements.include?(element) } end + # Alias for #excluding. + def without(*elements) + excluding(*elements) + end + # Convert an enumerable to an array based on the given key. # # [{ name: "David" }, { name: "Rafael" }, { name: "Aaron" }].pluck(:name) diff --git a/activesupport/lib/active_support/core_ext/hash.rb b/activesupport/lib/active_support/core_ext/hash.rb index c4b9e5f1a0..2f0901d853 100644 --- a/activesupport/lib/active_support/core_ext/hash.rb +++ b/activesupport/lib/active_support/core_ext/hash.rb @@ -2,6 +2,7 @@ require "active_support/core_ext/hash/conversions" require "active_support/core_ext/hash/deep_merge" +require "active_support/core_ext/hash/deep_transform_values" require "active_support/core_ext/hash/except" require "active_support/core_ext/hash/indifferent_access" require "active_support/core_ext/hash/keys" diff --git a/activesupport/lib/active_support/core_ext/hash/deep_transform_values.rb b/activesupport/lib/active_support/core_ext/hash/deep_transform_values.rb new file mode 100644 index 0000000000..720a1f67c8 --- /dev/null +++ b/activesupport/lib/active_support/core_ext/hash/deep_transform_values.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +class Hash + # Returns a new hash with all keys converted by the block operation. + # This includes the keys from the root hash and from all + # nested hashes and arrays. + # + # hash = { person: { name: 'Rob', age: '28' } } + # + # hash.deep_transform_values{ |value| value.to_s.upcase } + # # => {person: {name: "ROB", age: "28"}} + def deep_transform_values(&block) + _deep_transform_values_in_object(self, &block) + end + + # Destructively converts all values by using the block operation. + # This includes the values from the root hash and from all + # nested hashes and arrays. + def deep_transform_values!(&block) + _deep_transform_values_in_object!(self, &block) + end + + private + # support methods for deep transforming nested hashes and arrays + def _deep_transform_values_in_object(object, &block) + case object + when Hash + object.transform_values { |value| _deep_transform_values_in_object(value, &block) } + when Array + object.map { |e| _deep_transform_values_in_object(e, &block) } + else + yield(object) + end + end + + def _deep_transform_values_in_object!(object, &block) + case object + when Hash + object.transform_values! { |value| _deep_transform_values_in_object!(value, &block) } + when Array + object.map! { |e| _deep_transform_values_in_object!(e, &block) } + else + yield(object) + end + end +end diff --git a/activesupport/lib/active_support/core_ext/hash/except.rb b/activesupport/lib/active_support/core_ext/hash/except.rb index 6258610c98..5013812460 100644 --- a/activesupport/lib/active_support/core_ext/hash/except.rb +++ b/activesupport/lib/active_support/core_ext/hash/except.rb @@ -10,7 +10,7 @@ class Hash # This is useful for limiting a set of parameters to everything but a few known toggles: # @person.update(params[:person].except(:admin)) def except(*keys) - dup.except!(*keys) + slice(*self.keys - keys) end # Removes the given keys from hash and returns it. diff --git a/activesupport/lib/active_support/core_ext/kernel.rb b/activesupport/lib/active_support/core_ext/kernel.rb index 0f4356fbdd..7708069301 100644 --- a/activesupport/lib/active_support/core_ext/kernel.rb +++ b/activesupport/lib/active_support/core_ext/kernel.rb @@ -1,6 +1,5 @@ # frozen_string_literal: true -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" diff --git a/activesupport/lib/active_support/core_ext/kernel/agnostics.rb b/activesupport/lib/active_support/core_ext/kernel/agnostics.rb deleted file mode 100644 index 403b5f31f0..0000000000 --- a/activesupport/lib/active_support/core_ext/kernel/agnostics.rb +++ /dev/null @@ -1,13 +0,0 @@ -# frozen_string_literal: true - -class Object - # Makes backticks behave (somewhat more) similarly on all platforms. - # On win32 `nonexistent_command` raises Errno::ENOENT; on Unix, the - # spawned shell prints a message to stderr and sets $?. We emulate - # Unix on the former but not the latter. - def `(command) #:nodoc: - super - rescue Errno::ENOENT => e - STDERR.puts "#$0: #{e}" - end -end diff --git a/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb b/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb index 5850e0193f..cc1926e022 100644 --- a/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb +++ b/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb @@ -24,7 +24,7 @@ class Module # end # # => NameError: invalid attribute name: 1_Badname # - # If you want to opt out the creation on the instance reader method, pass + # To omit the instance reader method, pass # <tt>instance_reader: false</tt> or <tt>instance_accessor: false</tt>. # # module HairColors @@ -91,7 +91,7 @@ class Module # Person.new.hair_colors = [:blonde, :red] # HairColors.class_variable_get("@@hair_colors") # => [:blonde, :red] # - # If you want to opt out the instance writer method, pass + # To omit the instance writer method, pass # <tt>instance_writer: false</tt> or <tt>instance_accessor: false</tt>. # # module HairColors @@ -166,8 +166,8 @@ class Module # Citizen.new.hair_colors << :blue # Person.new.hair_colors # => [:brown, :black, :blonde, :red, :blue] # - # To opt out of the instance writer method, pass <tt>instance_writer: false</tt>. - # To opt out of the instance reader method, pass <tt>instance_reader: false</tt>. + # To omit the instance writer method, pass <tt>instance_writer: false</tt>. + # To omit the instance reader method, pass <tt>instance_reader: false</tt>. # # module HairColors # mattr_accessor :hair_colors, instance_writer: false, instance_reader: false @@ -180,7 +180,7 @@ class Module # Person.new.hair_colors = [:brown] # => NoMethodError # Person.new.hair_colors # => NoMethodError # - # Or pass <tt>instance_accessor: false</tt>, to opt out both instance methods. + # Or pass <tt>instance_accessor: false</tt>, to omit both instance methods. # # module HairColors # mattr_accessor :hair_colors, instance_accessor: false 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 cb996e9e6d..a6e87aeb68 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 @@ -25,7 +25,7 @@ class Module # end # # => NameError: invalid attribute name: 1_Badname # - # If you want to opt out of the creation of the instance reader method, pass + # To omit the instance reader method, pass # <tt>instance_reader: false</tt> or <tt>instance_accessor: false</tt>. # # class Current @@ -66,7 +66,7 @@ class Module # Current.user = "DHH" # Thread.current[:attr_Current_user] # => "DHH" # - # If you want to opt out of the creation of the instance writer method, pass + # To omit the instance writer method, pass # <tt>instance_writer: false</tt> or <tt>instance_accessor: false</tt>. # # class Current @@ -118,8 +118,8 @@ class Module # Customer.user # => "Rafael" # Account.user # => "DHH" # - # To opt out of the instance writer method, pass <tt>instance_writer: false</tt>. - # To opt out of the instance reader method, pass <tt>instance_reader: false</tt>. + # To omit the instance writer method, pass <tt>instance_writer: false</tt>. + # To omit the instance reader method, pass <tt>instance_reader: false</tt>. # # class Current # thread_mattr_accessor :user, instance_writer: false, instance_reader: false @@ -128,7 +128,7 @@ class Module # Current.new.user = "DHH" # => NoMethodError # Current.new.user # => NoMethodError # - # Or pass <tt>instance_accessor: false</tt>, to opt out both instance methods. + # Or pass <tt>instance_accessor: false</tt>, to omit both instance methods. # # class Current # thread_mattr_accessor :user, instance_accessor: false diff --git a/activesupport/lib/active_support/core_ext/range/compare_range.rb b/activesupport/lib/active_support/core_ext/range/compare_range.rb index 6f6d2a27bb..ea1dc29a76 100644 --- a/activesupport/lib/active_support/core_ext/range/compare_range.rb +++ b/activesupport/lib/active_support/core_ext/range/compare_range.rb @@ -3,9 +3,10 @@ module ActiveSupport module CompareWithRange # Extends the default Range#=== to support range comparisons. - # (1..5) === (1..5) # => true - # (1..5) === (2..3) # => true - # (1..5) === (2..6) # => false + # (1..5) === (1..5) # => true + # (1..5) === (2..3) # => true + # (1..5) === (1...6) # => true + # (1..5) === (2..6) # => false # # The native Range#=== behavior is untouched. # ('a'..'f') === ('c') # => true @@ -13,17 +14,20 @@ module ActiveSupport def ===(value) if value.is_a?(::Range) # 1...10 includes 1..9 but it does not include 1..10. + # 1..10 includes 1...11 but it does not include 1...12. operator = exclude_end? && !value.exclude_end? ? :< : :<= - super(value.first) && value.last.send(operator, last) + value_max = !exclude_end? && value.exclude_end? ? value.max : value.last + super(value.first) && value_max.send(operator, last) else super end end # Extends the default Range#include? to support range comparisons. - # (1..5).include?(1..5) # => true - # (1..5).include?(2..3) # => true - # (1..5).include?(2..6) # => false + # (1..5).include?(1..5) # => true + # (1..5).include?(2..3) # => true + # (1..5).include?(1...6) # => true + # (1..5).include?(2..6) # => false # # The native Range#include? behavior is untouched. # ('a'..'f').include?('c') # => true @@ -31,17 +35,20 @@ module ActiveSupport def include?(value) if value.is_a?(::Range) # 1...10 includes 1..9 but it does not include 1..10. + # 1..10 includes 1...11 but it does not include 1...12. operator = exclude_end? && !value.exclude_end? ? :< : :<= - super(value.first) && value.last.send(operator, last) + value_max = !exclude_end? && value.exclude_end? ? value.max : value.last + super(value.first) && value_max.send(operator, last) else super end end # Extends the default Range#cover? to support range comparisons. - # (1..5).cover?(1..5) # => true - # (1..5).cover?(2..3) # => true - # (1..5).cover?(2..6) # => false + # (1..5).cover?(1..5) # => true + # (1..5).cover?(2..3) # => true + # (1..5).cover?(1...6) # => true + # (1..5).cover?(2..6) # => false # # The native Range#cover? behavior is untouched. # ('a'..'f').cover?('c') # => true @@ -49,8 +56,10 @@ module ActiveSupport def cover?(value) if value.is_a?(::Range) # 1...10 covers 1..9 but it does not cover 1..10. + # 1..10 covers 1...11 but it does not cover 1...12. operator = exclude_end? && !value.exclude_end? ? :< : :<= - super(value.first) && value.last.send(operator, last) + value_max = !exclude_end? && value.exclude_end? ? value.max : value.last + super(value.first) && value_max.send(operator, last) else super end diff --git a/activesupport/lib/active_support/core_ext/string/inflections.rb b/activesupport/lib/active_support/core_ext/string/inflections.rb index 8af301734a..5eb8d9f99b 100644 --- a/activesupport/lib/active_support/core_ext/string/inflections.rb +++ b/activesupport/lib/active_support/core_ext/string/inflections.rb @@ -162,6 +162,11 @@ class String # Replaces special characters in a string so that it may be used as part of a 'pretty' URL. # + # If the optional parameter +locale+ is specified, + # the word will be parameterized as a word of that language. + # By default, this parameter is set to <tt>nil</tt> and it will use + # the configured <tt>I18n.locale</tt>. + # # class Person # def to_param # "#{id}-#{name.parameterize}" @@ -187,8 +192,8 @@ class String # # <%= link_to(@person.name, person_path) %> # # => <a href="/person/1-Donald-E-Knuth">Donald E. Knuth</a> - def parameterize(separator: "-", preserve_case: false) - ActiveSupport::Inflector.parameterize(self, separator: separator, preserve_case: preserve_case) + def parameterize(separator: "-", preserve_case: false, locale: nil) + ActiveSupport::Inflector.parameterize(self, separator: separator, preserve_case: preserve_case, locale: locale) end # Creates the name of a table like Rails does for models to table names. This method diff --git a/activesupport/lib/active_support/core_ext/string/output_safety.rb b/activesupport/lib/active_support/core_ext/string/output_safety.rb index 3a80de4617..638152626b 100644 --- a/activesupport/lib/active_support/core_ext/string/output_safety.rb +++ b/activesupport/lib/active_support/core_ext/string/output_safety.rb @@ -135,10 +135,12 @@ module ActiveSupport #:nodoc: class SafeBuffer < String UNSAFE_STRING_METHODS = %w( capitalize chomp chop delete delete_prefix delete_suffix - downcase gsub lstrip next reverse rstrip slice squeeze strip - sub succ swapcase tr tr_s unicode_normalize upcase + downcase lstrip next reverse rstrip slice squeeze strip + succ swapcase tr tr_s unicode_normalize upcase ) + UNSAFE_STRING_METHODS_WITH_BACKREF = %w(gsub sub) + alias_method :original_concat, :concat private :original_concat @@ -199,8 +201,12 @@ module ActiveSupport #:nodoc: super(html_escape_interpolated_argument(value)) end - def []=(index, value) - super(index, html_escape_interpolated_argument(value)) + def []=(*args) + if args.count == 3 + super(args[0], args[1], html_escape_interpolated_argument(args[2])) + else + super(args[0], html_escape_interpolated_argument(args[1])) + end end def +(other) @@ -249,11 +255,44 @@ module ActiveSupport #:nodoc: end end + UNSAFE_STRING_METHODS_WITH_BACKREF.each do |unsafe_method| + if unsafe_method.respond_to?(unsafe_method) + class_eval <<-EOT, __FILE__, __LINE__ + 1 + def #{unsafe_method}(*args, &block) # def gsub(*args, &block) + if block # if block + to_str.#{unsafe_method}(*args) { |*params| # to_str.gsub(*args) { |*params| + set_block_back_references(block, $~) # set_block_back_references(block, $~) + block.call(*params) # block.call(*params) + } # } + else # else + to_str.#{unsafe_method}(*args) # to_str.gsub(*args) + end # end + end # end + + def #{unsafe_method}!(*args, &block) # def gsub!(*args, &block) + @html_safe = false # @html_safe = false + if block # if block + super(*args) { |*params| # super(*args) { |*params| + set_block_back_references(block, $~) # set_block_back_references(block, $~) + block.call(*params) # block.call(*params) + } # } + else # else + super # super + end # end + end # end + EOT + end + end + private def html_escape_interpolated_argument(arg) (!html_safe? || arg.html_safe?) ? arg : CGI.escapeHTML(arg.to_s) end + + def set_block_back_references(block, match_data) + block.binding.eval("proc { |m| $~ = m }").call(match_data) + end end end diff --git a/activesupport/lib/active_support/core_ext/time/calculations.rb b/activesupport/lib/active_support/core_ext/time/calculations.rb index 120768dec5..f09a6271ad 100644 --- a/activesupport/lib/active_support/core_ext/time/calculations.rb +++ b/activesupport/lib/active_support/core_ext/time/calculations.rb @@ -170,8 +170,7 @@ class Time options[:hours] = options.fetch(:hours, 0) + 24 * partial_days end - d = to_date.advance(options) - d = d.gregorian if d.julian? + d = to_date.gregorian.advance(options) time_advanced_by_date = change(year: d.year, month: d.month, day: d.day) seconds_to_advance = \ options.fetch(:seconds, 0) + diff --git a/activesupport/lib/active_support/current_attributes.rb b/activesupport/lib/active_support/current_attributes.rb index 3145ff87a1..67ebe102d7 100644 --- a/activesupport/lib/active_support/current_attributes.rb +++ b/activesupport/lib/active_support/current_attributes.rb @@ -119,10 +119,16 @@ module ActiveSupport end end + # Calls this block before #reset is called on the instance. Used for resetting external collaborators that depend on current values. + def before_reset(&block) + set_callback :reset, :before, &block + 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 + alias_method :after_reset, :resets delegate :set, :reset, to: :instance diff --git a/activesupport/lib/active_support/dependencies.rb b/activesupport/lib/active_support/dependencies.rb index d5d00b5e6e..82f07c085e 100644 --- a/activesupport/lib/active_support/dependencies.rb +++ b/activesupport/lib/active_support/dependencies.rb @@ -70,6 +70,11 @@ module ActiveSupport #:nodoc: # only once. All directories in this set must also be present in +autoload_paths+. mattr_accessor :autoload_once_paths, default: [] + # This is a private set that collects all eager load paths during bootstrap. + # Useful for Zeitwerk integration. Its public interface is the config.* path + # accessors of each engine. + mattr_accessor :_eager_load_paths, default: Set.new + # 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. diff --git a/activesupport/lib/active_support/dependencies/zeitwerk_integration.rb b/activesupport/lib/active_support/dependencies/zeitwerk_integration.rb new file mode 100644 index 0000000000..d5dc7c2ff4 --- /dev/null +++ b/activesupport/lib/active_support/dependencies/zeitwerk_integration.rb @@ -0,0 +1,94 @@ +# frozen_string_literal: true + +require "set" +require "active_support/core_ext/string/inflections" + +module ActiveSupport + module Dependencies + module ZeitwerkIntegration # :nodoc: all + module Decorations + def clear + Dependencies.unload_interlock do + Rails.autoloaders.main.reload + rescue Zeitwerk::ReloadingDisabledError + raise "reloading is disabled because config.cache_classes is true" + end + end + + def constantize(cpath) + ActiveSupport::Inflector.constantize(cpath) + end + + def safe_constantize(cpath) + ActiveSupport::Inflector.safe_constantize(cpath) + end + + def to_unload?(cpath) + Rails.autoloaders.main.to_unload?(cpath) + end + + def verbose=(verbose) + l = verbose ? logger || Rails.logger : nil + Rails.autoloaders.each { |autoloader| autoloader.logger = l } + end + + def unhook! + :no_op + end + end + + module Inflector + def self.camelize(basename, _abspath) + basename.camelize + end + end + + class << self + def take_over(enable_reloading:) + setup_autoloaders(enable_reloading) + freeze_paths + decorate_dependencies + end + + private + + def setup_autoloaders(enable_reloading) + Dependencies.autoload_paths.each do |autoload_path| + # Zeitwerk only accepts existing directories in `push_dir` to + # prevent misconfigurations. + next unless File.directory?(autoload_path) + + autoloader = \ + autoload_once?(autoload_path) ? Rails.autoloaders.once : Rails.autoloaders.main + + autoloader.push_dir(autoload_path) + autoloader.do_not_eager_load(autoload_path) unless eager_load?(autoload_path) + end + + Rails.autoloaders.main.enable_reloading if enable_reloading + Rails.autoloaders.each(&:setup) + end + + def autoload_once?(autoload_path) + Dependencies.autoload_once_paths.include?(autoload_path) + end + + def eager_load?(autoload_path) + Dependencies._eager_load_paths.member?(autoload_path) + end + + def freeze_paths + Dependencies.autoload_paths.freeze + Dependencies.autoload_once_paths.freeze + Dependencies._eager_load_paths.freeze + end + + def decorate_dependencies + Dependencies.unhook! + Dependencies.singleton_class.prepend(Decorations) + Object.class_eval { alias_method :require_dependency, :require } + end + end + end + end +end diff --git a/activesupport/lib/active_support/descendants_tracker.rb b/activesupport/lib/active_support/descendants_tracker.rb index 05236d3162..fe0c6991aa 100644 --- a/activesupport/lib/active_support/descendants_tracker.rb +++ b/activesupport/lib/active_support/descendants_tracker.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require "weakref" + module ActiveSupport # This module provides an internal implementation to track descendants # which is faster than iterating through ObjectSpace. @@ -8,7 +10,8 @@ module ActiveSupport class << self def direct_descendants(klass) - @@direct_descendants[klass] || [] + descendants = @@direct_descendants[klass] + descendants ? descendants.to_a : [] end def descendants(klass) @@ -19,11 +22,18 @@ module ActiveSupport def clear if defined? ActiveSupport::Dependencies + # to_unload? is only defined in Zeitwerk mode. + to_unload = if Dependencies.respond_to?(:to_unload?) + ->(klass) { Dependencies.to_unload?(klass.name) } + else + ->(klass) { Dependencies.autoloaded?(klass) } + end + @@direct_descendants.each do |klass, descendants| - if ActiveSupport::Dependencies.autoloaded?(klass) + if to_unload[klass] @@direct_descendants.delete(klass) else - descendants.reject! { |v| ActiveSupport::Dependencies.autoloaded?(v) } + descendants.reject! { |v| to_unload[v] } end end else @@ -34,15 +44,17 @@ module ActiveSupport # This is the only method that is not thread safe, but is only ever called # during the eager loading phase. def store_inherited(klass, descendant) - (@@direct_descendants[klass] ||= []) << descendant + (@@direct_descendants[klass] ||= DescendantsArray.new) << descendant end private def accumulate_descendants(klass, acc) if direct_descendants = @@direct_descendants[klass] - acc.concat(direct_descendants) - direct_descendants.each { |direct_descendant| accumulate_descendants(direct_descendant, acc) } + direct_descendants.each do |direct_descendant| + acc << direct_descendant + accumulate_descendants(direct_descendant, acc) + end end end end @@ -59,5 +71,46 @@ module ActiveSupport def descendants DescendantsTracker.descendants(self) end + + # DescendantsArray is an array that contains weak references to classes. + class DescendantsArray # :nodoc: + include Enumerable + + def initialize + @refs = [] + end + + def initialize_copy(orig) + @refs = @refs.dup + end + + def <<(klass) + cleanup! + @refs << WeakRef.new(klass) + end + + def each + @refs.each do |ref| + yield ref.__getobj__ + rescue WeakRef::RefError + end + end + + def refs_size + @refs.size + end + + def cleanup! + @refs.delete_if { |ref| !ref.weakref_alive? } + end + + def reject! + @refs.reject! do |ref| + yield ref.__getobj__ + rescue WeakRef::RefError + true + end + end + end end end diff --git a/activesupport/lib/active_support/duration.rb b/activesupport/lib/active_support/duration.rb index 97b4634d7b..a30bd11a87 100644 --- a/activesupport/lib/active_support/duration.rb +++ b/activesupport/lib/active_support/duration.rb @@ -4,7 +4,6 @@ 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" module ActiveSupport # Provides accurate date and time measurements using Date#advance and diff --git a/activesupport/lib/active_support/encrypted_file.rb b/activesupport/lib/active_support/encrypted_file.rb index c66f1b557e..2b7db568a5 100644 --- a/activesupport/lib/active_support/encrypted_file.rb +++ b/activesupport/lib/active_support/encrypted_file.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require "pathname" +require "tmpdir" require "active_support/message_encryptor" module ActiveSupport @@ -67,7 +68,7 @@ module ActiveSupport write(updated_contents) if updated_contents != contents ensure - FileUtils.rm(tmp_path) if tmp_path.exist? + FileUtils.rm(tmp_path) if tmp_path&.exist? end diff --git a/activesupport/lib/active_support/evented_file_update_checker.rb b/activesupport/lib/active_support/evented_file_update_checker.rb index 54bc5c0ae5..3893b0de0e 100644 --- a/activesupport/lib/active_support/evented_file_update_checker.rb +++ b/activesupport/lib/active_support/evented_file_update_checker.rb @@ -131,7 +131,9 @@ module ActiveSupport ext = @ph.normalize_extension(file.extname) file.dirname.ascend do |dir| - if @dirs.fetch(dir, []).include?(ext) + matching = @dirs[dir] + + if matching && (matching.empty? || matching.include?(ext)) break true elsif dir == @lcsp || dir.root? break false diff --git a/activesupport/lib/active_support/gem_version.rb b/activesupport/lib/active_support/gem_version.rb index 8bf016e88c..cf17922d7d 100644 --- a/activesupport/lib/active_support/gem_version.rb +++ b/activesupport/lib/active_support/gem_version.rb @@ -10,7 +10,7 @@ module ActiveSupport MAJOR = 6 MINOR = 0 TINY = 0 - PRE = "beta1" + PRE = "beta3" STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".") end diff --git a/activesupport/lib/active_support/hash_with_indifferent_access.rb b/activesupport/lib/active_support/hash_with_indifferent_access.rb index f1af76019a..42ae7e9b7b 100644 --- a/activesupport/lib/active_support/hash_with_indifferent_access.rb +++ b/activesupport/lib/active_support/hash_with_indifferent_access.rb @@ -164,6 +164,19 @@ module ActiveSupport super(convert_key(key)) end + # Same as <tt>Hash#assoc</tt> where the key passed as argument can be + # either a string or a symbol: + # + # counters = ActiveSupport::HashWithIndifferentAccess.new + # counters[:foo] = 1 + # + # counters.assoc('foo') # => ["foo", 1] + # counters.assoc(:foo) # => ["foo", 1] + # counters.assoc(:zoo) # => nil + def assoc(key) + super(convert_key(key)) + end + # Same as <tt>Hash#fetch</tt> where the key passed as argument can be # either a string or a symbol: # @@ -212,8 +225,8 @@ module ActiveSupport # hash[:a] = 'x' # hash[:b] = 'y' # hash.values_at('a', 'b') # => ["x", "y"] - def values_at(*indices) - indices.collect { |key| self[convert_key(key)] } + def values_at(*keys) + super(*keys.map { |key| convert_key(key) }) end # Returns an array of the values at the specified indices, but also @@ -226,7 +239,7 @@ module ActiveSupport # 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) } + super(*indices.map { |key| convert_key(key) }, &block) end # Returns a shallow copy of the hash. @@ -280,6 +293,9 @@ module ActiveSupport super(convert_key(key)) end + def except(*keys) + slice(*self.keys - keys.map { |key| convert_key(key) }) + end alias_method :without, :except def stringify_keys!; self end diff --git a/activesupport/lib/active_support/inflector/transliterate.rb b/activesupport/lib/active_support/inflector/transliterate.rb index 0d2a17970f..ec6e9ccb59 100644 --- a/activesupport/lib/active_support/inflector/transliterate.rb +++ b/activesupport/lib/active_support/inflector/transliterate.rb @@ -51,19 +51,18 @@ module ActiveSupport # # Now you can have different transliterations for each locale: # - # I18n.locale = :en - # transliterate('Jürgen') + # transliterate('Jürgen', locale: :en) # # => "Jurgen" # - # I18n.locale = :de - # transliterate('Jürgen') + # transliterate('Jürgen', locale: :de) # # => "Juergen" - def transliterate(string, replacement = "?") + def transliterate(string, replacement = "?", locale: nil) raise ArgumentError, "Can only transliterate strings. Received #{string.class.name}" unless string.is_a?(String) I18n.transliterate( ActiveSupport::Multibyte::Unicode.tidy_bytes(string).unicode_normalize(:nfc), - replacement: replacement + replacement: replacement, + locale: locale ) end @@ -75,8 +74,8 @@ module ActiveSupport # # To use a custom separator, override the +separator+ argument. # - # parameterize("Donald E. Knuth", separator: '_') # => "donald_e_knuth" - # parameterize("^très|Jolie__ ", separator: '_') # => "tres_jolie" + # parameterize("Donald E. Knuth", separator: '_') # => "donald_e_knuth" + # parameterize("^très|Jolie__ ", separator: '_') # => "tres_jolie" # # To preserve the case of the characters in a string, use the +preserve_case+ argument. # @@ -85,13 +84,17 @@ module ActiveSupport # # It preserves dashes and underscores unless they are used as separators: # - # parameterize("^très|Jolie__ ") # => "tres-jolie__" - # parameterize("^très|Jolie-- ", separator: "_") # => "tres_jolie--" - # parameterize("^très_Jolie-- ", separator: ".") # => "tres_jolie--" + # parameterize("^très|Jolie__ ") # => "tres-jolie__" + # parameterize("^très|Jolie-- ", separator: "_") # => "tres_jolie--" + # parameterize("^très_Jolie-- ", separator: ".") # => "tres_jolie--" # - def parameterize(string, separator: "-", preserve_case: false) + # If the optional parameter +locale+ is specified, + # the word will be parameterized as a word of that language. + # By default, this parameter is set to <tt>nil</tt> and it will use + # the configured <tt>I18n.locale<tt>. + def parameterize(string, separator: "-", preserve_case: false, locale: nil) # Replace accented chars with their ASCII equivalents. - parameterized_string = transliterate(string) + parameterized_string = transliterate(string, locale: locale) # Turn unwanted chars into the separator. parameterized_string.gsub!(/[^a-z0-9\-_]+/i, separator) diff --git a/activesupport/lib/active_support/message_encryptor.rb b/activesupport/lib/active_support/message_encryptor.rb index 6f7302e732..7d6f8937f0 100644 --- a/activesupport/lib/active_support/message_encryptor.rb +++ b/activesupport/lib/active_support/message_encryptor.rb @@ -53,7 +53,7 @@ module ActiveSupport # crypt.encrypt_and_sign(parcel, expires_in: 1.month) # crypt.encrypt_and_sign(doowad, expires_at: Time.now.end_of_year) # - # Then the messages can be verified and returned upto the expire time. + # Then the messages can be verified and returned up to the expire time. # Thereafter, verifying returns +nil+. # # === Rotating keys diff --git a/activesupport/lib/active_support/message_verifier.rb b/activesupport/lib/active_support/message_verifier.rb index 64c557bec6..c4a4afe95f 100644 --- a/activesupport/lib/active_support/message_verifier.rb +++ b/activesupport/lib/active_support/message_verifier.rb @@ -71,7 +71,7 @@ module ActiveSupport # @verifier.generate(parcel, expires_in: 1.month) # @verifier.generate(doowad, expires_at: Time.now.end_of_year) # - # Then the messages can be verified and returned upto the expire time. + # Then the messages can be verified and returned up to the expire time. # Thereafter, the +verified+ method returns +nil+ while +verify+ raises # <tt>ActiveSupport::MessageVerifier::InvalidSignature</tt>. # diff --git a/activesupport/lib/active_support/notifications.rb b/activesupport/lib/active_support/notifications.rb index 7ccc333463..d9e93b530c 100644 --- a/activesupport/lib/active_support/notifications.rb +++ b/activesupport/lib/active_support/notifications.rb @@ -153,6 +153,15 @@ module ActiveSupport # # ActiveSupport::Notifications.unsubscribe("render") # + # Subscribers using a regexp or other pattern-matching object will remain subscribed + # to all events that match their original pattern, unless those events match a string + # passed to `unsubscribe`: + # + # subscriber = ActiveSupport::Notifications.subscribe(/render/) { } + # ActiveSupport::Notifications.unsubscribe('render_template.action_view') + # subscriber.matches?('render_template.action_view') # => false + # subscriber.matches?('render_partial.action_view') # => true + # # == Default Queue # # Notifications ships with a queue implementation that consumes and publishes events diff --git a/activesupport/lib/active_support/notifications/fanout.rb b/activesupport/lib/active_support/notifications/fanout.rb index 4e4ca70942..8812b67f63 100644 --- a/activesupport/lib/active_support/notifications/fanout.rb +++ b/activesupport/lib/active_support/notifications/fanout.rb @@ -2,6 +2,7 @@ require "mutex_m" require "concurrent/map" +require "set" module ActiveSupport module Notifications @@ -13,16 +14,22 @@ module ActiveSupport include Mutex_m def initialize - @subscribers = [] + @string_subscribers = Hash.new { |h, k| h[k] = [] } + @other_subscribers = [] @listeners_for = Concurrent::Map.new super end - def subscribe(pattern = nil, block = Proc.new) - subscriber = Subscribers.new pattern, block + def subscribe(pattern = nil, callable = nil, &block) + subscriber = Subscribers.new(pattern, callable || block) synchronize do - @subscribers << subscriber - @listeners_for.clear + if String === pattern + @string_subscribers[pattern] << subscriber + @listeners_for.delete(pattern) + else + @other_subscribers << subscriber + @listeners_for.clear + end end subscriber end @@ -31,12 +38,19 @@ module ActiveSupport synchronize do case subscriber_or_name when String - @subscribers.reject! { |s| s.matches?(subscriber_or_name) } + @string_subscribers[subscriber_or_name].clear + @listeners_for.delete(subscriber_or_name) + @other_subscribers.each { |sub| sub.unsubscribe!(subscriber_or_name) } else - @subscribers.delete(subscriber_or_name) + pattern = subscriber_or_name.try(:pattern) + if String === pattern + @string_subscribers[pattern].delete(subscriber_or_name) + @listeners_for.delete(pattern) + else + @other_subscribers.delete(subscriber_or_name) + @listeners_for.clear + end end - - @listeners_for.clear end end @@ -56,7 +70,8 @@ module ActiveSupport # this is correctly done double-checked locking (Concurrent::Map's lookups have volatile semantics) @listeners_for[name] || synchronize do # use synchronisation when accessing @subscribers - @listeners_for[name] ||= @subscribers.select { |s| s.subscribed_to?(name) } + @listeners_for[name] ||= + @string_subscribers[name] + @other_subscribers.select { |s| s.subscribed_to?(name) } end end @@ -100,9 +115,33 @@ module ActiveSupport end end + class Matcher #:nodoc: + attr_reader :pattern, :exclusions + + def self.wrap(pattern) + return pattern if String === pattern + new(pattern) + end + + def initialize(pattern) + @pattern = pattern + @exclusions = Set.new + end + + def unsubscribe!(name) + exclusions << -name if pattern === name + end + + def ===(name) + pattern === name && !exclusions.include?(name) + end + end + class Evented #:nodoc: + attr_reader :pattern + def initialize(pattern, delegate) - @pattern = pattern + @pattern = Matcher.wrap(pattern) @delegate = delegate @can_publish = delegate.respond_to?(:publish) end @@ -122,11 +161,15 @@ module ActiveSupport end def subscribed_to?(name) - @pattern === name + pattern === name end def matches?(name) - @pattern && @pattern === name + pattern && pattern === name + end + + def unsubscribe!(name) + pattern.unsubscribe!(name) end end @@ -137,13 +180,13 @@ module ActiveSupport def start(name, id, payload) timestack = Thread.current[:_timestack] ||= [] - timestack.push Time.now + timestack.push Concurrent.monotonic_time end def finish(name, id, payload) timestack = Thread.current[:_timestack] started = timestack.pop - @delegate.call(name, started, Time.now, id, payload) + @delegate.call(name, started, Concurrent.monotonic_time, id, payload) end end @@ -189,6 +232,10 @@ module ActiveSupport true end + def unsubscribe!(*) + false + end + alias :matches? :=== end end diff --git a/activesupport/lib/active_support/notifications/instrumenter.rb b/activesupport/lib/active_support/notifications/instrumenter.rb index 125c06f37a..12546511a8 100644 --- a/activesupport/lib/active_support/notifications/instrumenter.rb +++ b/activesupport/lib/active_support/notifications/instrumenter.rb @@ -13,14 +13,15 @@ module ActiveSupport @notifier = notifier end - # Instrument the given block by measuring the time taken to execute it - # and publish it. Notice that events get sent even if an error occurs - # in the passed-in block. + # Given a block, instrument it by measuring the time taken to execute + # and publish it. Without a block, simply send a message via the + # notifier. Notice that events get sent even if an error occurs in the + # passed-in block. def instrument(name, payload = {}) # some of the listeners might have state listeners_state = start name, payload begin - yield payload + yield payload if block_given? rescue Exception => e payload[:exception] = [e.class.name, e.message] payload[:exception_object] = e @@ -67,9 +68,8 @@ module ActiveSupport @transaction_id = transaction_id @end = ending @children = [] - @duration = nil - @cpu_time_start = nil - @cpu_time_finish = nil + @cpu_time_start = 0 + @cpu_time_finish = 0 @allocation_count_start = 0 @allocation_count_finish = 0 end @@ -124,7 +124,7 @@ module ActiveSupport # # @event.duration # => 1000.138 def duration - @duration ||= 1000.0 * (self.end - time) + 1000.0 * (self.end - time) end def <<(event) @@ -137,7 +137,7 @@ module ActiveSupport private def now - Process.clock_gettime(Process::CLOCK_MONOTONIC) + Concurrent.monotonic_time end if clock_gettime_supported? diff --git a/activesupport/lib/active_support/security_utils.rb b/activesupport/lib/active_support/security_utils.rb index 20b6b9cd3f..5e455fca57 100644 --- a/activesupport/lib/active_support/security_utils.rb +++ b/activesupport/lib/active_support/security_utils.rb @@ -24,7 +24,7 @@ module ActiveSupport # The values are first processed by SHA256, so that we don't leak length info # via timing attacks. def secure_compare(a, b) - fixed_length_secure_compare(::Digest::SHA256.hexdigest(a), ::Digest::SHA256.hexdigest(b)) && a == b + fixed_length_secure_compare(::Digest::SHA256.digest(a), ::Digest::SHA256.digest(b)) && a == b end module_function :secure_compare end diff --git a/activesupport/lib/active_support/subscriber.rb b/activesupport/lib/active_support/subscriber.rb index f3e902f9dd..c3cd175a52 100644 --- a/activesupport/lib/active_support/subscriber.rb +++ b/activesupport/lib/active_support/subscriber.rb @@ -24,6 +24,10 @@ module ActiveSupport # After configured, whenever a "sql.active_record" notification is published, # it will properly dispatch the event (ActiveSupport::Notifications::Event) to # the +sql+ method. + # + # We can detach a subscriber as well: + # + # ActiveRecord::StatsSubscriber.detach_from(:active_record) class Subscriber class << self # Attach the subscriber to a namespace. @@ -40,6 +44,25 @@ module ActiveSupport end end + # Detach the subscriber from a namespace. + def detach_from(namespace, notifier = ActiveSupport::Notifications) + @namespace = namespace + @subscriber = find_attached_subscriber + @notifier = notifier + + return unless subscriber + + subscribers.delete(subscriber) + + # Remove event subscribers of all existing methods on the class. + subscriber.public_methods(false).each do |event| + remove_event_subscriber(event) + end + + # Reset notifier so that event subscribers will not add for new methods added to the class. + @notifier = nil + end + # Adds event subscribers for all new methods added to the class. def method_added(event) # Only public methods are added as subscribers, and only if a notifier @@ -58,15 +81,41 @@ module ActiveSupport attr_reader :subscriber, :notifier, :namespace def add_event_subscriber(event) # :doc: - return if %w{ start finish }.include?(event.to_s) + return if invalid_event?(event.to_s) - pattern = "#{event}.#{namespace}" + pattern = prepare_pattern(event) # Don't add multiple subscribers (eg. if methods are redefined). - return if subscriber.patterns.include?(pattern) + return if pattern_subscribed?(pattern) + + subscriber.patterns[pattern] = notifier.subscribe(pattern, subscriber) + end + + def remove_event_subscriber(event) # :doc: + return if invalid_event?(event.to_s) + + pattern = prepare_pattern(event) + + return unless pattern_subscribed?(pattern) + + notifier.unsubscribe(subscriber.patterns[pattern]) + subscriber.patterns.delete(pattern) + end + + def find_attached_subscriber + subscribers.find { |attached_subscriber| attached_subscriber.instance_of?(self) } + end + + def invalid_event?(event) + %w{ start finish }.include?(event.to_s) + end + + def prepare_pattern(event) + "#{event}.#{namespace}" + end - subscriber.patterns << pattern - notifier.subscribe(pattern, subscriber) + def pattern_subscribed?(pattern) + subscriber.patterns.key?(pattern) end end @@ -74,7 +123,7 @@ module ActiveSupport def initialize @queue_key = [self.class.name, object_id].join "-" - @patterns = [] + @patterns = {} super end diff --git a/activesupport/lib/active_support/testing/parallelization.rb b/activesupport/lib/active_support/testing/parallelization.rb index 63440069b1..08285b2f52 100644 --- a/activesupport/lib/active_support/testing/parallelization.rb +++ b/activesupport/lib/active_support/testing/parallelization.rb @@ -79,7 +79,9 @@ module ActiveSupport klass = job[0] method = job[1] reporter = job[2] - result = Minitest.run_one_method(klass, method) + result = klass.with_info_handler reporter do + Minitest.run_one_method(klass, method) + end begin queue.record(reporter, result) diff --git a/activesupport/test/abstract_unit.rb b/activesupport/test/abstract_unit.rb index 62356e4d46..01e60abd99 100644 --- a/activesupport/test/abstract_unit.rb +++ b/activesupport/test/abstract_unit.rb @@ -40,3 +40,5 @@ class ActiveSupport::TestCase skip message if defined?(JRUBY_VERSION) end end + +require_relative "../../tools/test_common" diff --git a/activesupport/test/cache/behaviors/connection_pool_behavior.rb b/activesupport/test/cache/behaviors/connection_pool_behavior.rb index 5e66e0b202..aed04d07d4 100644 --- a/activesupport/test/cache/behaviors/connection_pool_behavior.rb +++ b/activesupport/test/cache/behaviors/connection_pool_behavior.rb @@ -7,7 +7,7 @@ module ConnectionPoolBehavior threads = [] emulating_latency do - cache = ActiveSupport::Cache.lookup_store(store, { pool_size: 2, pool_timeout: 1 }.merge(store_options)) + cache = ActiveSupport::Cache.lookup_store(*store, { pool_size: 2, pool_timeout: 1 }.merge(store_options)) cache.clear assert_raises Timeout::Error do @@ -32,7 +32,7 @@ module ConnectionPoolBehavior threads = [] emulating_latency do - cache = ActiveSupport::Cache.lookup_store(store, store_options) + cache = ActiveSupport::Cache.lookup_store(*store, store_options) cache.clear assert_nothing_raised do diff --git a/activesupport/test/cache/stores/file_store_test.rb b/activesupport/test/cache/stores/file_store_test.rb index f6855bb308..0364d9ab64 100644 --- a/activesupport/test/cache/stores/file_store_test.rb +++ b/activesupport/test/cache/stores/file_store_test.rb @@ -101,7 +101,7 @@ class FileStoreTest < ActiveSupport::TestCase 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_empty Dir.entries(sub_cache_dir).reject { |f| ActiveSupport::Cache::FileStore::EXCLUDED_DIRS.include?(f) } + assert_empty Dir.children(sub_cache_dir) end def test_log_exception_when_cache_read_fails diff --git a/activesupport/test/cache/stores/mem_cache_store_test.rb b/activesupport/test/cache/stores/mem_cache_store_test.rb index f426a37c66..0e472f5a1a 100644 --- a/activesupport/test/cache/stores/mem_cache_store_test.rb +++ b/activesupport/test/cache/stores/mem_cache_store_test.rb @@ -25,8 +25,9 @@ end class MemCacheStoreTest < ActiveSupport::TestCase begin - ss = Dalli::Client.new("localhost:11211").stats - raise Dalli::DalliError unless ss["localhost:11211"] + servers = ENV["MEMCACHE_SERVERS"] || "localhost:11211" + ss = Dalli::Client.new(servers).stats + raise Dalli::DalliError unless ss[servers] MEMCACHE_UP = true rescue Dalli::DalliError @@ -37,8 +38,8 @@ class MemCacheStoreTest < ActiveSupport::TestCase 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) + @cache = ActiveSupport::Cache.lookup_store(*store, expires_in: 60) + @peek = ActiveSupport::Cache.lookup_store(*store) @data = @cache.instance_variable_get(:@data) @cache.clear @cache.silence! @@ -56,21 +57,21 @@ class MemCacheStoreTest < ActiveSupport::TestCase include FailureSafetyBehavior def test_raw_values - cache = ActiveSupport::Cache.lookup_store(:mem_cache_store, raw: true) + cache = ActiveSupport::Cache.lookup_store(*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 = ActiveSupport::Cache.lookup_store(*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 = ActiveSupport::Cache.lookup_store(*store, raw: true) cache.clear cache.with_local_cache do cache.write("foo", 2) @@ -79,7 +80,7 @@ class MemCacheStoreTest < ActiveSupport::TestCase end def test_increment_expires_in - cache = ActiveSupport::Cache.lookup_store(:mem_cache_store, raw: true) + cache = ActiveSupport::Cache.lookup_store(*store, raw: true) cache.clear assert_called_with cache.instance_variable_get(:@data), :incr, [ "foo", 1, 60 ] do cache.increment("foo", 1, expires_in: 60) @@ -87,7 +88,7 @@ class MemCacheStoreTest < ActiveSupport::TestCase end def test_decrement_expires_in - cache = ActiveSupport::Cache.lookup_store(:mem_cache_store, raw: true) + cache = ActiveSupport::Cache.lookup_store(*store, raw: true) cache.clear assert_called_with cache.instance_variable_get(:@data), :decr, [ "foo", 1, 60 ] do cache.decrement("foo", 1, expires_in: 60) @@ -95,7 +96,7 @@ class MemCacheStoreTest < ActiveSupport::TestCase end def test_local_cache_raw_values_with_marshal - cache = ActiveSupport::Cache.lookup_store(:mem_cache_store, raw: true) + cache = ActiveSupport::Cache.lookup_store(*store, raw: true) cache.clear cache.with_local_cache do cache.write("foo", Marshal.dump([])) @@ -114,7 +115,7 @@ class MemCacheStoreTest < ActiveSupport::TestCase private def store - :mem_cache_store + [:mem_cache_store, ENV["MEMCACHE_SERVERS"] || "localhost:11211"] end def emulating_latency diff --git a/activesupport/test/cache/stores/redis_cache_store_test.rb b/activesupport/test/cache/stores/redis_cache_store_test.rb index 305a2c184d..790534cd3c 100644 --- a/activesupport/test/cache/stores/redis_cache_store_test.rb +++ b/activesupport/test/cache/stores/redis_cache_store_test.rb @@ -140,6 +140,12 @@ module ActiveSupport::Cache::RedisCacheStoreTests end end + def test_fetch_multi_without_names + assert_not_called(@cache.redis, :mget) do + @cache.fetch_multi() { } + end + end + def test_increment_expires_in assert_called_with @cache.redis, :incrby, [ "#{@namespace}:foo", 1 ] do assert_called_with @cache.redis, :expire, [ "#{@namespace}:foo", 60 ] do @@ -187,7 +193,7 @@ module ActiveSupport::Cache::RedisCacheStoreTests private def store - :redis_cache_store + [:redis_cache_store] end def emulating_latency @@ -204,7 +210,7 @@ module ActiveSupport::Cache::RedisCacheStoreTests class RedisDistributedConnectionPoolBehaviourTest < ConnectionPoolBehaviourTest private def store_options - { url: %w[ redis://localhost:6379/0 redis://localhost:6379/0 ] } + { url: [ENV["REDIS_URL"] || "redis://localhost:6379/0"] * 2 } end end diff --git a/activesupport/test/core_ext/array/access_test.rb b/activesupport/test/core_ext/array/access_test.rb index 8c217023cf..427b058925 100644 --- a/activesupport/test/core_ext/array/access_test.rb +++ b/activesupport/test/core_ext/array/access_test.rb @@ -32,6 +32,18 @@ class AccessTest < ActiveSupport::TestCase assert_equal array[-2], array.second_to_last end + def test_including + assert_equal [1, 2, 3, 4, 5], [1, 2, 4].including(3, 5).sort + assert_equal [1, 2, 3, 4, 5], [1, 2, 4].including([3, 5]).sort + assert_equal [[0, 1], [1, 0]], [[0, 1]].including([[1, 0]]) + end + + def test_excluding + assert_equal [1, 2, 4], [1, 2, 3, 4, 5].excluding(3, 5) + assert_equal [1, 2, 4], [1, 2, 3, 4, 5].excluding([3, 5]) + assert_equal [[0, 1]], [[0, 1], [1, 0]].excluding([[1, 0]]) + end + def test_without assert_equal [1, 2, 4], [1, 2, 3, 4, 5].without(3, 5) end diff --git a/activesupport/test/core_ext/enumerable_test.rb b/activesupport/test/core_ext/enumerable_test.rb index b63464a36a..381b5a1f32 100644 --- a/activesupport/test/core_ext/enumerable_test.rb +++ b/activesupport/test/core_ext/enumerable_test.rb @@ -217,11 +217,18 @@ class EnumerableTests < ActiveSupport::TestCase assert_equal false, GenericEnumerable.new([ 1 ]).exclude?(1) end + def test_excluding + assert_equal [1, 2, 4], GenericEnumerable.new((1..5).to_a).excluding(3, 5) + assert_equal [3, 4, 5], GenericEnumerable.new((1..5).to_a).excluding([1, 2]) + assert_equal [[0, 1]], GenericEnumerable.new([[0, 1], [1, 0]]).excluding([[1, 0]]) + assert_equal [1, 2, 4], (1..5).to_a.excluding(3, 5) + assert_equal [1, 2, 4], (1..5).to_set.excluding(3, 5) + assert_equal({ foo: 1, baz: 3 }, { foo: 1, bar: 2, baz: 3 }.excluding(:bar)) + end + def test_without assert_equal [1, 2, 4], GenericEnumerable.new((1..5).to_a).without(3, 5) - assert_equal [1, 2, 4], (1..5).to_a.without(3, 5) - assert_equal [1, 2, 4], (1..5).to_set.without(3, 5) - assert_equal({ foo: 1, baz: 3 }, { foo: 1, bar: 2, baz: 3 }.without(:bar)) + assert_equal [3, 4, 5], GenericEnumerable.new((1..5).to_a).without([1, 2]) end def test_pluck diff --git a/activesupport/test/core_ext/hash_ext_test.rb b/activesupport/test/core_ext/hash_ext_test.rb index e8e0a1ae72..8572d56722 100644 --- a/activesupport/test/core_ext/hash_ext_test.rb +++ b/activesupport/test/core_ext/hash_ext_test.rb @@ -33,6 +33,8 @@ class HashExtTest < ActiveSupport::TestCase h = {} assert_respond_to h, :deep_transform_keys assert_respond_to h, :deep_transform_keys! + assert_respond_to h, :deep_transform_values + assert_respond_to h, :deep_transform_values! assert_respond_to h, :symbolize_keys assert_respond_to h, :symbolize_keys! assert_respond_to h, :deep_symbolize_keys @@ -78,6 +80,31 @@ class HashExtTest < ActiveSupport::TestCase assert_equal({ "a" => { b: { "c" => 3 } } }, @nested_mixed) end + def test_deep_transform_values + assert_equal({ "a" => "1", "b" => "2" }, @strings.deep_transform_values { |value| value.to_s }) + assert_equal({ "a" => { "b" => { "c" => "3" } } }, @nested_strings.deep_transform_values { |value| value.to_s }) + assert_equal({ "a" => [ { "b" => "2" }, { "c" => "3" }, "4" ] }, @string_array_of_hashes.deep_transform_values { |value| value.to_s }) + end + + def test_deep_transform_values_not_mutates + transformed_hash = @nested_mixed.deep_dup + transformed_hash.deep_transform_values { |value| value.to_s } + assert_equal @nested_mixed, transformed_hash + end + + def test_deep_transform_values! + assert_equal({ "a" => "1", "b" => "2" }, @strings.deep_transform_values! { |value| value.to_s }) + assert_equal({ "a" => { "b" => { "c" => "3" } } }, @nested_strings.deep_transform_values! { |value| value.to_s }) + assert_equal({ "a" => [ { "b" => "2" }, { "c" => "3" }, "4" ] }, @string_array_of_hashes.deep_transform_values! { |value| value.to_s }) + end + + def test_deep_transform_values_with_bang_mutates + transformed_hash = @nested_mixed.deep_dup + transformed_hash.deep_transform_values! { |value| value.to_s } + assert_equal({ "a" => { b: { "c" => "3" } } }, transformed_hash) + assert_equal({ "a" => { b: { "c" => 3 } } }, @nested_mixed) + end + def test_symbolize_keys assert_equal @symbols, @symbols.symbolize_keys assert_equal @symbols, @strings.symbolize_keys diff --git a/activesupport/test/core_ext/range_ext_test.rb b/activesupport/test/core_ext/range_ext_test.rb index 4b8efb8a93..16d6a4c2f2 100644 --- a/activesupport/test/core_ext/range_ext_test.rb +++ b/activesupport/test/core_ext/range_ext_test.rb @@ -57,7 +57,7 @@ class RangeTest < ActiveSupport::TestCase end def test_should_include_other_with_exclusive_end - assert((1..10).include?(1...10)) + assert((1..10).include?(1...11)) end def test_should_compare_identical_inclusive @@ -69,7 +69,7 @@ class RangeTest < ActiveSupport::TestCase end def test_should_compare_other_with_exclusive_end - assert((1..10) === (1...10)) + assert((1..10) === (1...11)) end def test_exclusive_end_should_not_include_identical_with_inclusive_end @@ -93,6 +93,10 @@ class RangeTest < ActiveSupport::TestCase assert range.method(:include?) != range.method(:cover?) end + def test_should_cover_other_with_exclusive_end + assert((1..10).cover?(1...11)) + end + def test_overlaps_on_time time_range_1 = Time.utc(2005, 12, 10, 15, 30)..Time.utc(2005, 12, 10, 17, 30) time_range_2 = Time.utc(2005, 12, 10, 17, 00)..Time.utc(2005, 12, 10, 18, 00) diff --git a/activesupport/test/core_ext/string_ext_test.rb b/activesupport/test/core_ext/string_ext_test.rb index 2468fe3603..4ffa33aa61 100644 --- a/activesupport/test/core_ext/string_ext_test.rb +++ b/activesupport/test/core_ext/string_ext_test.rb @@ -206,6 +206,12 @@ class StringInflectionsTest < ActiveSupport::TestCase end end + def test_parameterize_with_locale + word = "Fünf autos" + I18n.backend.store_translations(:de, i18n: { transliterate: { rule: { "ü" => "ue" } } }) + assert_equal("fuenf-autos", word.parameterize(locale: :de)) + end + def test_humanize UnderscoreToHuman.each do |underscore, human| assert_equal(human, underscore.humanize) diff --git a/activesupport/test/core_ext/time_ext_test.rb b/activesupport/test/core_ext/time_ext_test.rb index 7078f3506d..590b81b770 100644 --- a/activesupport/test/core_ext/time_ext_test.rb +++ b/activesupport/test/core_ext/time_ext_test.rb @@ -514,6 +514,8 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase assert_equal Time.local(1582, 10, 15, 15, 15, 10), Time.local(1582, 10, 14, 15, 15, 10).advance(days: 1) assert_equal Time.local(1582, 10, 5, 15, 15, 10), Time.local(1582, 10, 4, 15, 15, 10).advance(days: 1) assert_equal Time.local(1582, 10, 4, 15, 15, 10), Time.local(1582, 10, 5, 15, 15, 10).advance(days: -1) + assert_equal Time.local(999, 10, 4, 15, 15, 10), Time.local(1000, 10, 4, 15, 15, 10).advance(years: -1) + assert_equal Time.local(1000, 10, 4, 15, 15, 10), Time.local(999, 10, 4, 15, 15, 10).advance(years: 1) end def test_last_week diff --git a/activesupport/test/current_attributes_test.rb b/activesupport/test/current_attributes_test.rb index 1669f08f68..adbdc646bc 100644 --- a/activesupport/test/current_attributes_test.rb +++ b/activesupport/test/current_attributes_test.rb @@ -3,13 +3,18 @@ require "abstract_unit" class CurrentAttributesTest < ActiveSupport::TestCase - Person = Struct.new(:name, :time_zone) + Person = Struct.new(:id, :name, :time_zone) class Current < ActiveSupport::CurrentAttributes attribute :world, :account, :person, :request delegate :time_zone, to: :person - resets { Time.zone = "UTC" } + before_reset { Session.previous = person.try(:id) } + + resets do + Time.zone = "UTC" + Session.current = nil + end def account=(account) super @@ -19,6 +24,7 @@ class CurrentAttributesTest < ActiveSupport::TestCase def person=(person) super Time.zone = person.try(:time_zone) + Session.current = person.try(:id) end def request @@ -30,9 +36,14 @@ class CurrentAttributesTest < ActiveSupport::TestCase end end + class Session < ActiveSupport::CurrentAttributes + attribute :current, :previous + end + setup do @original_time_zone = Time.zone Current.reset + Session.reset end teardown do @@ -56,16 +67,28 @@ class CurrentAttributesTest < ActiveSupport::TestCase end test "set auxiliary class via overwritten method" do - Current.person = Person.new("David", "Central Time (US & Canada)") + Current.person = Person.new(42, "David", "Central Time (US & Canada)") assert_equal "Central Time (US & Canada)", Time.zone.name + assert_equal 42, Session.current end - test "resets auxiliary class via callback" do - Current.person = Person.new("David", "Central Time (US & Canada)") + test "resets auxiliary classes via callback" do + Current.person = Person.new(42, "David", "Central Time (US & Canada)") assert_equal "Central Time (US & Canada)", Time.zone.name Current.reset assert_equal "UTC", Time.zone.name + assert_nil Session.current + end + + test "set auxiliary class based on current attributes via before callback" do + Current.person = Person.new(42, "David", "Central Time (US & Canada)") + assert_nil Session.previous + assert_equal 42, Session.current + + Current.reset + assert_equal 42, Session.previous + assert_nil Session.current end test "set attribute only via scope" do @@ -92,13 +115,13 @@ class CurrentAttributesTest < ActiveSupport::TestCase end test "delegation" do - Current.person = Person.new("David", "Central Time (US & Canada)") + Current.person = Person.new(42, "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)") + Current.person = Person.new(42, "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 diff --git a/activesupport/test/dependencies_test.rb b/activesupport/test/dependencies_test.rb index b1b3070891..d4e709137e 100644 --- a/activesupport/test/dependencies_test.rb +++ b/activesupport/test/dependencies_test.rb @@ -841,7 +841,7 @@ class DependenciesTest < ActiveSupport::TestCase remove_constants(:C) end - def test_new_contants_in_without_constants + def test_new_constants_in_without_constants assert_equal [], (ActiveSupport::Dependencies.new_constants_in(Object) { }) assert ActiveSupport::Dependencies.constant_watch_stack.all? { |k, v| v.empty? } end diff --git a/activesupport/test/descendants_tracker_test_cases.rb b/activesupport/test/descendants_tracker_test_cases.rb index 2c94c3c56c..f8752688d2 100644 --- a/activesupport/test/descendants_tracker_test_cases.rb +++ b/activesupport/test/descendants_tracker_test_cases.rb @@ -27,6 +27,15 @@ module DescendantsTrackerTestCases assert_equal_sets [], Child2.descendants end + def test_descendants_with_garbage_collected_classes + 1.times do + child_klass = Class.new(Parent) + assert_equal_sets [Child1, Grandchild1, Grandchild2, Child2, child_klass], Parent.descendants + end + GC.start + assert_equal_sets [Child1, Grandchild1, Grandchild2, Child2], Parent.descendants + end + def test_direct_descendants assert_equal_sets [Child1, Child2], Parent.direct_descendants assert_equal_sets [Grandchild1, Grandchild2], Child1.direct_descendants diff --git a/activesupport/test/file_update_checker_shared_tests.rb b/activesupport/test/file_update_checker_shared_tests.rb index 72683816b3..84d89fa0a7 100644 --- a/activesupport/test/file_update_checker_shared_tests.rb +++ b/activesupport/test/file_update_checker_shared_tests.rb @@ -186,6 +186,18 @@ module FileUpdateCheckerSharedTests assert_equal 1, i end + test "should execute the block if files change in a watched directory any extensions" do + i = 0 + + checker = new_checker([], tmpdir => []) { i += 1 } + + touch(tmpfile("foo.rb")) + wait + + assert checker.execute_if_updated + assert_equal 1, i + end + test "should execute the block if files change in a watched directory several extensions" do i = 0 diff --git a/activesupport/test/hash_with_indifferent_access_test.rb b/activesupport/test/hash_with_indifferent_access_test.rb index f81e0dc70f..1d6a07ec5f 100644 --- a/activesupport/test/hash_with_indifferent_access_test.rb +++ b/activesupport/test/hash_with_indifferent_access_test.rb @@ -447,6 +447,14 @@ class HashWithIndifferentAccessTest < ActiveSupport::TestCase assert_instance_of ActiveSupport::HashWithIndifferentAccess, indifferent_strings end + def test_indifferent_assoc + indifferent_strings = ActiveSupport::HashWithIndifferentAccess.new(@strings) + key, value = indifferent_strings.assoc(:a) + + assert_equal("a", key) + assert_equal(1, value) + end + def test_indifferent_compact hash_contain_nil_value = @strings.merge("z" => nil) hash = ActiveSupport::HashWithIndifferentAccess.new(hash_contain_nil_value) @@ -478,7 +486,7 @@ class HashWithIndifferentAccessTest < ActiveSupport::TestCase assert_equal @strings, roundtrip assert_equal "1234", roundtrip.default - # Ensure nested hashes are not HashWithIndiffereneAccess + # Ensure nested hashes are not HashWithIndifferentAccess new_to_hash = @nested_mixed.with_indifferent_access.to_hash assert_not new_to_hash.instance_of?(HashWithIndifferentAccess) assert_not new_to_hash["a"].instance_of?(HashWithIndifferentAccess) diff --git a/activesupport/test/inflector_test.rb b/activesupport/test/inflector_test.rb index c3e1faff5d..ddf765a220 100644 --- a/activesupport/test/inflector_test.rb +++ b/activesupport/test/inflector_test.rb @@ -304,6 +304,12 @@ class InflectorTest < ActiveSupport::TestCase end end + def test_parameterize_with_locale + word = "Fünf autos" + I18n.backend.store_translations(:de, i18n: { transliterate: { rule: { "ü" => "ue" } } }) + assert_equal("fuenf-autos", ActiveSupport::Inflector.parameterize(word, locale: :de)) + end + def test_classify ClassNameToTableName.each do |class_name, table_name| assert_equal(class_name, ActiveSupport::Inflector.classify(table_name)) diff --git a/activesupport/test/notifications/evented_notification_test.rb b/activesupport/test/notifications/evented_notification_test.rb index 4beb8194b9..ab2a9b8659 100644 --- a/activesupport/test/notifications/evented_notification_test.rb +++ b/activesupport/test/notifications/evented_notification_test.rb @@ -84,6 +84,39 @@ module ActiveSupport [:finish, "hi", 1, {}] ], listener.events end + + def test_listen_to_regexp + notifier = Fanout.new + listener = Listener.new + notifier.subscribe(/[a-z]*.world/, listener) + notifier.start("hi.world", 1, {}) + notifier.finish("hi.world", 2, {}) + notifier.start("hello.world", 1, {}) + notifier.finish("hello.world", 2, {}) + + assert_equal [ + [:start, "hi.world", 1, {}], + [:finish, "hi.world", 2, {}], + [:start, "hello.world", 1, {}], + [:finish, "hello.world", 2, {}] + ], listener.events + end + + def test_listen_to_regexp_with_exclusions + notifier = Fanout.new + listener = Listener.new + notifier.subscribe(/[a-z]*.world/, listener) + notifier.unsubscribe("hi.world") + notifier.start("hi.world", 1, {}) + notifier.finish("hi.world", 2, {}) + notifier.start("hello.world", 1, {}) + notifier.finish("hello.world", 2, {}) + + assert_equal [ + [:start, "hello.world", 1, {}], + [:finish, "hello.world", 2, {}] + ], listener.events + end end end end diff --git a/activesupport/test/notifications/instrumenter_test.rb b/activesupport/test/notifications/instrumenter_test.rb index d5c9e82e9f..9729ad5c89 100644 --- a/activesupport/test/notifications/instrumenter_test.rb +++ b/activesupport/test/notifications/instrumenter_test.rb @@ -44,6 +44,12 @@ module ActiveSupport assert_equal Hash[result: 2], payload end + def test_instrument_works_without_a_block + instrumenter.instrument("no.block", payload) + assert_equal 1, notifier.finishes.size + assert_equal "no.block", notifier.finishes.first.first + end + def test_start instrumenter.start("foo", payload) assert_equal [["foo", instrumenter.id, payload]], notifier.starts diff --git a/activesupport/test/notifications_test.rb b/activesupport/test/notifications_test.rb index 4e0aef2cc7..0af59764b5 100644 --- a/activesupport/test/notifications_test.rb +++ b/activesupport/test/notifications_test.rb @@ -81,7 +81,7 @@ module Notifications assert_equal expected, events end - def test_subsribing_to_instrumentation_while_inside_it + def test_subscribing_to_instrumentation_while_inside_it # the repro requires that there are no evented subscribers for the "foo" event, # so we have to duplicate some of the setup code old_notifier = ActiveSupport::Notifications.notifier @@ -128,6 +128,25 @@ module Notifications assert_equal [["named.subscription", :foo], ["named.subscription", :foo]], @events end + def test_unsubscribing_by_name_leaves_regexp_matched_subscriptions + @matched_events = [] + @notifier.subscribe(/subscription/) { |*args| @matched_events << event(*args) } + @notifier.publish("named.subscription", :before) + @notifier.wait + [@events, @named_events, @matched_events].each do |collector| + assert_includes(collector, ["named.subscription", :before]) + end + @notifier.unsubscribe("named.subscription") + @notifier.publish("named.subscription", :after) + @notifier.publish("other.subscription", :after) + @notifier.wait + assert_includes(@events, ["named.subscription", :after]) + assert_includes(@events, ["other.subscription", :after]) + assert_includes(@matched_events, ["other.subscription", :after]) + assert_not_includes(@matched_events, ["named.subscription", :after]) + assert_not_includes(@named_events, ["named.subscription", :after]) + end + private def event(*args) args @@ -283,7 +302,7 @@ module Notifications class EventTest < TestCase def test_events_are_initialized_with_details - time = Time.now + time = Concurrent.monotonic_time event = event(:foo, time, time + 0.01, random_id, {}) assert_equal :foo, event.name @@ -291,15 +310,24 @@ module Notifications assert_in_delta 10.0, event.duration, 0.00001 end + def test_event_cpu_time_and_idle_time_when_start_and_finish_is_not_called + time = Concurrent.monotonic_time + event = event(:foo, time, time + 0.01, random_id, {}) + + assert_equal 0, event.cpu_time + assert_in_delta 10.0, event.idle_time, 0.00001 + end + + def test_events_consumes_information_given_as_payload - event = event(:foo, Time.now, Time.now + 1, random_id, payload: :bar) + event = event(:foo, Concurrent.monotonic_time, Concurrent.monotonic_time + 1, random_id, payload: :bar) assert_equal Hash[payload: :bar], event.payload end def test_event_is_parent_based_on_children - time = Time.utc(2009, 01, 01, 0, 0, 1) + time = Concurrent.monotonic_time - parent = event(:foo, Time.utc(2009), Time.utc(2009) + 100, random_id, {}) + parent = event(:foo, Concurrent.monotonic_time, Concurrent.monotonic_time + 100, random_id, {}) child = event(:foo, time, time + 10, random_id, {}) not_child = event(:foo, time, time + 100, random_id, {}) diff --git a/activesupport/test/safe_buffer_test.rb b/activesupport/test/safe_buffer_test.rb index 49a3951623..b1a1c2d390 100644 --- a/activesupport/test/safe_buffer_test.rb +++ b/activesupport/test/safe_buffer_test.rb @@ -112,6 +112,38 @@ class SafeBufferTest < ActiveSupport::TestCase end end + test "can assign value into zero-index" do + buffer = ActiveSupport::SafeBuffer.new("012345") + + buffer[0] = "<" + + assert_equal "<12345", buffer + end + + test "can assign value into non zero-index" do + buffer = ActiveSupport::SafeBuffer.new("012345") + + buffer[2] = "<" + + assert_equal "01<345", buffer + end + + test "can assign value into slice" do + buffer = ActiveSupport::SafeBuffer.new("012345") + + buffer[0, 3] = "<" + + assert_equal "<345", buffer + end + + test "can assign value into offset slice" do + buffer = ActiveSupport::SafeBuffer.new("012345") + + buffer[1, 3] = "<" + + assert_equal "0<45", buffer + end + test "Should escape dirty buffers on add" do clean = "hello".html_safe @buffer.gsub!("", "<>") @@ -151,7 +183,7 @@ class SafeBufferTest < ActiveSupport::TestCase assert_equal "", ActiveSupport::SafeBuffer.new("foo").clone_empty end - test "clone_empty keeps the original dirtyness" do + test "clone_empty keeps the original dirtiness" do assert_predicate @buffer.clone_empty, :html_safe? assert_not_predicate @buffer.gsub!("", "").clone_empty, :html_safe? end @@ -216,4 +248,22 @@ class SafeBufferTest < ActiveSupport::TestCase x = "Hello".html_safe assert_nil x[/a/, 1] end + + test "Should set back references" do + a = "foo123".html_safe + a2 = a.sub(/([a-z]+)([0-9]+)/) { $2 + $1 } + assert_equal "123foo", a2 + assert_not_predicate a2, :html_safe? + a.sub!(/([a-z]+)([0-9]+)/) { $2 + $1 } + assert_equal "123foo", a + assert_not_predicate a, :html_safe? + + b = "foo123 bar456".html_safe + b2 = b.gsub(/([a-z]+)([0-9]+)/) { $2 + $1 } + assert_equal "123foo 456bar", b2 + assert_not_predicate b2, :html_safe? + b.gsub!(/([a-z]+)([0-9]+)/) { $2 + $1 } + assert_equal "123foo 456bar", b + assert_not_predicate b, :html_safe? + end end diff --git a/activesupport/test/share_lock_test.rb b/activesupport/test/share_lock_test.rb index 30a1ddad3f..a40c813fe3 100644 --- a/activesupport/test/share_lock_test.rb +++ b/activesupport/test/share_lock_test.rb @@ -38,7 +38,7 @@ class ShareLockTest < ActiveSupport::TestCase end end - def test_multiple_exlusives_are_able_to_progress + def test_multiple_exclusives_are_able_to_progress with_thread_waiting_in_lock_section(:sharing) do |sharing_thread_release_latch| exclusive_threads = (1..2).map do Thread.new do diff --git a/activesupport/test/subscriber_test.rb b/activesupport/test/subscriber_test.rb index 6b012e43af..bc8d8f1c13 100644 --- a/activesupport/test/subscriber_test.rb +++ b/activesupport/test/subscriber_test.rb @@ -23,6 +23,21 @@ class TestSubscriber < ActiveSupport::Subscriber end end +class TestSubscriber2 < ActiveSupport::Subscriber + attach_to :doodle + detach_from :doodle + + cattr_reader :events + + def self.clear + @@events = [] + end + + def open_party(event) + events << event + end +end + # Monkey patch subscriber to test that only one subscriber per method is added. class TestSubscriber remove_method :open_party @@ -34,6 +49,7 @@ end class SubscriberTest < ActiveSupport::TestCase def setup TestSubscriber.clear + TestSubscriber2.clear end def test_attaches_subscribers @@ -53,4 +69,11 @@ class SubscriberTest < ActiveSupport::TestCase assert_equal [], TestSubscriber.events end + + def test_detaches_subscribers + ActiveSupport::Notifications.instrument("open_party.doodle") + + assert_equal [], TestSubscriber2.events + assert_equal 1, TestSubscriber.events.size + end end diff --git a/activesupport/test/time_travel_test.rb b/activesupport/test/time_travel_test.rb index 9c61ab0ab5..a1f84bf69e 100644 --- a/activesupport/test/time_travel_test.rb +++ b/activesupport/test/time_travel_test.rb @@ -148,7 +148,7 @@ class TimeTravelTest < ActiveSupport::TestCase end end - def test_travel_to_will_reset_the_usec_to_avoid_mysql_rouding + def test_travel_to_will_reset_the_usec_to_avoid_mysql_rounding Time.stub(:now, Time.now) do travel_to Time.utc(2014, 10, 10, 10, 10, 50, 999999) do assert_equal 50, Time.now.sec diff --git a/activesupport/test/time_zone_test.rb b/activesupport/test/time_zone_test.rb index 6d45a6726d..f948c363df 100644 --- a/activesupport/test/time_zone_test.rb +++ b/activesupport/test/time_zone_test.rb @@ -32,7 +32,7 @@ class TimeZoneTest < ActiveSupport::TestCase end end - def test_period_for_local_with_ambigiuous_time + def test_period_for_local_with_ambiguous_time zone = ActiveSupport::TimeZone["Moscow"] period = zone.period_for_local(Time.utc(2015, 1, 1)) assert_equal period, zone.period_for_local(Time.utc(2014, 10, 26, 1, 0, 0)) diff --git a/activesupport/test/transliterate_test.rb b/activesupport/test/transliterate_test.rb index 7d19447598..9e29a93ea0 100644 --- a/activesupport/test/transliterate_test.rb +++ b/activesupport/test/transliterate_test.rb @@ -30,6 +30,12 @@ class TransliterateTest < ActiveSupport::TestCase I18n.locale = default_locale end + def test_transliterate_respects_the_locale_argument + char = [117, 776].pack("U*") # "ü" as ASCII "u" plus COMBINING DIAERESIS + I18n.backend.store_translations(:de, i18n: { transliterate: { rule: { "ü" => "ue" } } }) + assert_equal "ue", ActiveSupport::Inflector.transliterate(char, locale: :de) + end + def test_transliterate_should_allow_a_custom_replacement_char assert_equal "a*b", ActiveSupport::Inflector.transliterate("a索b", "*") end |