diff options
Diffstat (limited to 'activesupport')
271 files changed, 5459 insertions, 3255 deletions
diff --git a/activesupport/.gitignore b/activesupport/.gitignore new file mode 100644 index 0000000000..8cde8514fd --- /dev/null +++ b/activesupport/.gitignore @@ -0,0 +1 @@ +/test/fixtures/isolation_test/ diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md index abbadd404f..c46b56833d 100644 --- a/activesupport/CHANGELOG.md +++ b/activesupport/CHANGELOG.md @@ -1,486 +1,342 @@ -* Introduced `ActiveSupport::Digest` that allows to specify hash function implementation - and defaults to `Digest::MD5`. +* New autoloading based on [Zeitwerk](https://github.com/fxn/zeitwerk). - Replaced calls to `::Digest::MD5.hexdigest` with calls to `ActiveSupport::Digest.hexdigest`. + *Xavier Noria* - *Dmitri Dolguikh* - -## Rails 5.2.0.beta2 (November 28, 2017) ## +* Revise `ActiveSupport::Notifications.unsubscribe` to correctly handle Regex or other multiple-pattern subscribers. -* No changes. + *Zach Kemp* +* Add `before_reset` callback to `CurrentAttributes` and define `after_reset` as an alias of `resets` for symmetry. -## Rails 5.2.0.beta1 (November 27, 2017) ## + *Rosa Gutierrez* -* Changed default behaviour of `ActiveSupport::SecurityUtils.secure_compare`, - to make it not leak length information even for variable length string. +* Remove the `` Kernel#` `` override that suppresses ENOENT and accidentally returns nil on Unix systems. - Renamed old `ActiveSupport::SecurityUtils.secure_compare` to `fixed_length_secure_compare`, - and started raising `ArgumentError` in case of length mismatch of passed strings. + *Akinori Musha* - *Vipul A M* +* Add `ActiveSupport::HashWithIndifferentAccess#assoc`. -* Make `ActiveSupport::TimeZone.all` return only time zones that are in - `ActiveSupport::TimeZone::MAPPING`. + `assoc` can now be called with either a string or a symbol. - Fixes #7245. + *Stefan Schüßler* - *Chris LaRose* +* Add `Hash#deep_transform_values`, and `Hash#deep_transform_values!`. -* MemCacheStore: Support expiring counters. + *Guillermo Iguaran* - Pass `expires_in: [seconds]` to `#increment` and `#decrement` options - to set the Memcached TTL (time-to-live) if the counter doesn't exist. - If the counter exists, Memcached doesn't extend its expiry when it's - incremented or decremented. +## Rails 6.0.0.beta1 (January 18, 2019) ## - ``` - Rails.cache.increment("my_counter", 1, expires_in: 2.minutes) - ``` - - *Takumasa Ochi* - -* Handle `TZInfo::AmbiguousTime` errors - - Make `ActiveSupport::TimeWithZone` match Ruby's handling of ambiguous - times by choosing the later period, e.g. - - Ruby: - ``` - ENV["TZ"] = "Europe/Moscow" - Time.local(2014, 10, 26, 1, 0, 0) # => 2014-10-26 01:00:00 +0300 - ``` - - Before: - ``` - >> "2014-10-26 01:00:00".in_time_zone("Moscow") - TZInfo::AmbiguousTime: 26/10/2014 01:00 is an ambiguous local time. - ``` - - After: - ``` - >> "2014-10-26 01:00:00".in_time_zone("Moscow") - => Sun, 26 Oct 2014 01:00:00 MSK +03:00 - ``` - - Fixes #17395. - - *Andrew White* - -* Redis cache store. +* Remove deprecated `Module#reachable?` method. - ``` - # Defaults to `redis://localhost:6379/0`. Only use for dev/test. - config.cache_store = :redis_cache_store - - # Supports all common cache store options (:namespace, :compress, - # :compress_threshold, :expires_in, :race_condition_ttl) and all - # Redis options. - cache_password = Rails.application.secrets.redis_cache_password - config.cache_store = :redis_cache_store, driver: :hiredis, - namespace: 'myapp-cache', compress: true, timeout: 1, - url: "redis://:#{cache_password}@myapp-cache-1:6379/0" - - # Supports Redis::Distributed with multiple hosts - config.cache_store = :redis_cache_store, driver: :hiredis - namespace: 'myapp-cache', compress: true, - url: %w[ - redis://myapp-cache-1:6379/0 - redis://myapp-cache-1:6380/0 - redis://myapp-cache-2:6379/0 - redis://myapp-cache-2:6380/0 - redis://myapp-cache-3:6379/0 - redis://myapp-cache-3:6380/0 - ] - - # Or pass a builder block - config.cache_store = :redis_cache_store, - namespace: 'myapp-cache', compress: true, - redis: -> { Redis.new … } - ``` + *Rafael Mendonça França* - Deployment note: Take care to use a *dedicated Redis cache* rather - than pointing this at your existing Redis server. It won't cope well - with mixed usage patterns and it won't expire cache entries by default. +* Remove deprecated `#acronym_regex` method from `Inflections`. - Redis cache server setup guide: https://redis.io/topics/lru-cache + *Rafael Mendonça França* - *Jeremy Daer* +* Fix `String#safe_constantize` throwing a `LoadError` for incorrectly cased constant references. -* Cache: Enable compression by default for values > 1kB. + *Keenan Brock* - Compression has long been available, but opt-in and at a 16kB threshold. - It wasn't enabled by default due to CPU cost. Today it's cheap and typical - cache data is eminently compressible, such as HTML or JSON fragments. - Compression dramatically reduces Memcached/Redis mem usage, which means - the same cache servers can store more data, which means higher hit rates. +* Preserve key order passed to `ActiveSupport::CacheStore#fetch_multi`. - To disable compression, pass `compress: false` to the initializer. + `fetch_multi(*names)` now returns its results in the same order as the `*names` requested, rather than returning cache hits followed by cache misses. - *Jeremy Daer* + *Gannon McGibbon* -* Allow `Range#include?` on TWZ ranges +* If the same block is `included` multiple times for a Concern, an exception is no longer raised. - In #11474 we prevented TWZ ranges being iterated over which matched - Ruby's handling of Time ranges and as a consequence `include?` - stopped working with both Time ranges and TWZ ranges. However in - ruby/ruby@b061634 support was added for `include?` to use `cover?` - for 'linear' objects. Since we have no way of making Ruby consider - TWZ instances as 'linear' we have to override `Range#include?`. + *Mark J. Titorenko*, *Vlad Bokov* - Fixes #30799. +* Fix bug where `#to_options` for `ActiveSupport::HashWithIndifferentAccess` + would not act as alias for `#symbolize_keys`. - *Andrew White* + *Nick Weiland* -* Fix acronym support in `humanize` +* Improve the logic that detects non-autoloaded constants. - Acronym inflections are stored with lowercase keys in the hash but - the match wasn't being lowercased before being looked up in the hash. - This shouldn't have any performance impact because before it would - fail to find the acronym and perform the `downcase` operation anyway. + *Jan Habermann*, *Xavier Noria* - Fixes #31052. +* Deprecate `ActiveSupport::Multibyte::Unicode#pack_graphemes(array)` and `ActiveSuppport::Multibyte::Unicode#unpack_graphemes(string)` + in favor of `array.flatten.pack("U*")` and `string.scan(/\X/).map(&:codepoints)`, respectively. - *Andrew White* + *Francesco Rodríguez* -* Add same method signature for `Time#prev_year` and `Time#next_year` - in accordance with `Date#prev_year`, `Date#next_year`. +* Deprecate `ActiveSupport::Multibyte::Chars.consumes?` in favor of `String#is_utf8?`. - Allows pass argument for `Time#prev_year` and `Time#next_year`. + *Francesco Rodríguez* - Before: +* Fix duration being rounded to a full second. ``` - Time.new(2017, 9, 16, 17, 0).prev_year # => 2016-09-16 17:00:00 +0300 - Time.new(2017, 9, 16, 17, 0).prev_year(1) - # => ArgumentError: wrong number of arguments (given 1, expected 0) - - Time.new(2017, 9, 16, 17, 0).next_year # => 2018-09-16 17:00:00 +0300 - Time.new(2017, 9, 16, 17, 0).next_year(1) - # => ArgumentError: wrong number of arguments (given 1, expected 0) + time = DateTime.parse("2018-1-1") + time += 0.51.seconds ``` + Will now correctly add 0.51 second and not 1 full second. - After: - ``` - Time.new(2017, 9, 16, 17, 0).prev_year # => 2016-09-16 17:00:00 +0300 - Time.new(2017, 9, 16, 17, 0).prev_year(1) # => 2016-09-16 17:00:00 +0300 + *Edouard Chin* - Time.new(2017, 9, 16, 17, 0).next_year # => 2018-09-16 17:00:00 +0300 - Time.new(2017, 9, 16, 17, 0).next_year(1) # => 2018-09-16 17:00:00 +0300 - ``` +* Deprecate `ActiveSupport::Multibyte::Unicode#normalize` and `ActiveSuppport::Multibyte::Chars#normalize` + in favor of `String#unicode_normalize` - *bogdanvlviv* + *Francesco Rodríguez* -* Add same method signature for `Time#prev_month` and `Time#next_month` - in accordance with `Date#prev_month`, `Date#next_month`. +* Deprecate `ActiveSupport::Multibyte::Unicode#downcase/upcase/swapcase` in favor of + `String#downcase/upcase/swapcase`. - Allows pass argument for `Time#prev_month` and `Time#next_month`. + *Francesco Rodríguez* - Before: - ``` - Time.new(2017, 9, 16, 17, 0).prev_month # => 2017-08-16 17:00:00 +0300 - Time.new(2017, 9, 16, 17, 0).prev_month(1) - # => ArgumentError: wrong number of arguments (given 1, expected 0) +* Add `ActiveSupport::ParameterFilter`. - Time.new(2017, 9, 16, 17, 0).next_month # => 2017-10-16 17:00:00 +0300 - Time.new(2017, 9, 16, 17, 0).next_month(1) - # => ArgumentError: wrong number of arguments (given 1, expected 0) - ``` + *Yoshiyuki Kinjo* - After: - ``` - Time.new(2017, 9, 16, 17, 0).prev_month # => 2017-08-16 17:00:00 +0300 - Time.new(2017, 9, 16, 17, 0).prev_month(1) # => 2017-08-16 17:00:00 +0300 +* Rename `Module#parent`, `Module#parents`, and `Module#parent_name` to + `module_parent`, `module_parents`, and `module_parent_name`. - Time.new(2017, 9, 16, 17, 0).next_month # => 2017-10-16 17:00:00 +0300 - Time.new(2017, 9, 16, 17, 0).next_month(1) # => 2017-10-16 17:00:00 +0300 - ``` + *Gannon McGibbon* - *bogdanvlviv* +* Deprecate the use of `LoggerSilence` in favor of `ActiveSupport::LoggerSilence` -* Add same method signature for `Time#prev_day` and `Time#next_day` - in accordance with `Date#prev_day`, `Date#next_day`. + *Edouard Chin* - Allows pass argument for `Time#prev_day` and `Time#next_day`. +* Deprecate using negative limits in `String#first` and `String#last`. - Before: - ``` - Time.new(2017, 9, 16, 17, 0).prev_day # => 2017-09-15 17:00:00 +0300 - Time.new(2017, 9, 16, 17, 0).prev_day(1) - # => ArgumentError: wrong number of arguments (given 1, expected 0) + *Gannon McGibbon*, *Eric Turner* - Time.new(2017, 9, 16, 17, 0).next_day # => 2017-09-17 17:00:00 +0300 - Time.new(2017, 9, 16, 17, 0).next_day(1) - # => ArgumentError: wrong number of arguments (given 1, expected 0) - ``` - - After: - ``` - Time.new(2017, 9, 16, 17, 0).prev_day # => 2017-09-15 17:00:00 +0300 - Time.new(2017, 9, 16, 17, 0).prev_day(1) # => 2017-09-15 17:00:00 +0300 - - Time.new(2017, 9, 16, 17, 0).next_day # => 2017-09-17 17:00:00 +0300 - Time.new(2017, 9, 16, 17, 0).next_day(1) # => 2017-09-17 17:00:00 +0300 - ``` - - *bogdanvlviv* - -* `IO#to_json` now returns the `to_s` representation, rather than - attempting to convert to an array. This fixes a bug where `IO#to_json` - would raise an `IOError` when called on an unreadable object. +* Fix bug where `#without` for `ActiveSupport::HashWithIndifferentAccess` would fail + with symbol arguments - Fixes #26132. + *Abraham Chan* - *Paul Kuruvilla* +* Treat `#delete_prefix`, `#delete_suffix` and `#unicode_normalize` results as non-`html_safe`. + Ensure safety of arguments for `#insert`, `#[]=` and `#replace` calls on `html_safe` Strings. -* Remove deprecated `halt_callback_chains_on_return_false` option. + *Janosch Müller* - *Rafael Mendonça França* +* Changed `ActiveSupport::TaggedLogging.new` to return a new logger instance instead + of mutating the one received as parameter. -* Remove deprecated `:if` and `:unless` string filter for callbacks. + *Thierry Joyal* - *Rafael Mendonça França* +* Define `unfreeze_time` as an alias of `travel_back` in `ActiveSupport::Testing::TimeHelpers`. -* `Hash#slice` now falls back to Ruby 2.5+'s built-in definition if defined. + The alias is provided for symmetry with `freeze_time`. - *Akira Matsuda* + *Ryan Davidson* -* Deprecate `secrets.secret_token`. +* Add support for tracing constant autoloads. Just throw - The architecture for secrets had a big upgrade between Rails 3 and Rails 4, - when the default changed from using `secret_token` to `secret_key_base`. + ActiveSupport::Dependencies.logger = Rails.logger + ActiveSupport::Dependencies.verbose = true - `secret_token` has been soft deprecated in documentation for four years - but is still in place to support apps created before Rails 4. - Deprecation warnings have been added to help developers upgrade their - applications to `secret_key_base`. + in an initializer. - *claudiob*, *Kasper Timm Hansen* + *Xavier Noria* -* Return an instance of `HashWithIndifferentAccess` from `HashWithIndifferentAccess#transform_keys`. +* Maintain `html_safe?` on html_safe strings when sliced. - *Yuji Yaginuma* + string = "<div>test</div>".html_safe + string[-1..1].html_safe? # => true -* Add key rotation support to `MessageEncryptor` and `MessageVerifier` + *Elom Gomez*, *Yumin Wong* - This change introduces a `rotate` method to both the `MessageEncryptor` and - `MessageVerifier` classes. This method accepts the same arguments and - options as the given classes' constructor. The `encrypt_and_verify` method - for `MessageEncryptor` and the `verified` method for `MessageVerifier` also - accept an optional keyword argument `:on_rotation` block which is called - when a rotated instance is used to decrypt or verify the message. +* Add `Array#extract!`. - *Michael J Coyne* + The method removes and returns the elements for which the block returns a true value. + If no block is given, an Enumerator is returned instead. -* Deprecate `Module#reachable?` method. + numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + odd_numbers = numbers.extract! { |number| number.odd? } # => [1, 3, 5, 7, 9] + numbers # => [0, 2, 4, 6, 8] *bogdanvlviv* -* Add `config/credentials.yml.enc` to store production app secrets. - - Allows saving any authentication credentials for third party services - directly in repo encrypted with `config/master.key` or `ENV["RAILS_MASTER_KEY"]`. - - This will eventually replace `Rails.application.secrets` and the encrypted - secrets introduced in Rails 5.1. - - *DHH*, *Kasper Timm Hansen* - -* Add `ActiveSupport::EncryptedFile` and `ActiveSupport::EncryptedConfiguration`. - - Allows for stashing encrypted files or configuration directly in repo by - encrypting it with a key. - - Backs the new credentials setup above, but can also be used independently. +* Support not to cache `nil` for `ActiveSupport::Cache#fetch`. - *DHH*, *Kasper Timm Hansen* + cache.fetch('bar', skip_nil: true) { nil } + cache.exist?('bar') # => false -* `Module#delegate_missing_to` now raises `DelegationError` if target is nil, - similar to `Module#delegate`. + *Martin Hong* - *Anton Khamets* +* Add "event object" support to the notification system. + Before this change, end users were forced to create hand made artisanal + event objects on their own, like this: -* Update `String#camelize` to provide feedback when wrong option is passed + ActiveSupport::Notifications.subscribe('wait') do |*args| + @event = ActiveSupport::Notifications::Event.new(*args) + end - `String#camelize` was returning nil without any feedback when an - invalid option was passed as a parameter. + ActiveSupport::Notifications.instrument('wait') do + sleep 1 + end - Previously: + @event.duration # => 1000.138 - 'one_two'.camelize(true) - # => nil + After this change, if the block passed to `subscribe` only takes one + parameter, the framework will yield an event object to the block. Now + end users are no longer required to make their own: - Now: + ActiveSupport::Notifications.subscribe('wait') do |event| + @event = event + end - 'one_two'.camelize(true) - # => ArgumentError: Invalid option, use either :upper or :lower. + ActiveSupport::Notifications.instrument('wait') do + sleep 1 + end - *Ricardo Díaz* + p @event.allocations # => 7 + p @event.cpu_time # => 0.256 + p @event.idle_time # => 1003.2399 -* Fix modulo operations involving durations + Now you can enjoy event objects without making them yourself. Neat! - Rails 5.1 introduced `ActiveSupport::Duration::Scalar` as a wrapper - around numeric values as a way of ensuring a duration was the outcome of - an expression. However, the implementation was missing support for modulo - operations. This support has now been added and should result in a duration - being returned from expressions involving modulo operations. + *Aaron "t.lo" Patterson* - Prior to Rails 5.1: +* Add cpu_time, idle_time, and allocations to Event. - 5.minutes % 2.minutes - # => 60 + *Eileen M. Uchitelle*, *Aaron Patterson* - Now: +* RedisCacheStore: support key expiry in increment/decrement. - 5.minutes % 2.minutes - # => 1 minute + Pass `:expires_in` to `#increment` and `#decrement` to set a Redis EXPIRE on the key. - Fixes #29603 and #29743. + If the key is already set to expire, RedisCacheStore won't extend its expiry. - *Sayan Chakraborty*, *Andrew White* + Rails.cache.increment("some_key", 1, expires_in: 2.minutes) -* Fix division where a duration is the denominator + *Jason Lee* - PR #29163 introduced a change in behavior when a duration was the denominator - in a calculation - this was incorrect as dividing by a duration should always - return a `Numeric`. The behavior of previous versions of Rails has been restored. +* Allow `Range#===` and `Range#cover?` on Range. - Fixes #29592. + `Range#cover?` can now accept a range argument like `Range#include?` and + `Range#===`. `Range#===` works correctly on Ruby 2.6. `Range#include?` is moved + into a new file, with these two methods. - *Andrew White* + *Requiring active_support/core_ext/range/include_range is now deprecated.* + *Use `require "active_support/core_ext/range/compare_range"` instead.* -* Add purpose and expiry support to `ActiveSupport::MessageVerifier` & - `ActiveSupport::MessageEncryptor`. + *utilum* - For instance, to ensure a message is only usable for one intended purpose: +* Add `index_with` to Enumerable. - token = @verifier.generate("x", purpose: :shipping) + Allows creating a hash from an enumerable with the value from a passed block + or a default argument. - @verifier.verified(token, purpose: :shipping) # => "x" - @verifier.verified(token) # => nil + %i( title body ).index_with { |attr| post.public_send(attr) } + # => { title: "hey", body: "what's up?" } - Or make it expire after a set time: + %i( title body ).index_with(nil) + # => { title: nil, body: nil } - @verifier.generate("x", expires_in: 1.month) - @verifier.generate("y", expires_at: Time.now.end_of_year) + Closely linked with `index_by`, which creates a hash where the keys are extracted from a block. - Showcased with `ActiveSupport::MessageVerifier`, but works the same for - `ActiveSupport::MessageEncryptor`'s `encrypt_and_sign` and `decrypt_and_verify`. + *Kasper Timm Hansen* - Pull requests: #29599, #29854 +* Fix bug where `ActiveSupport::Timezone.all` would fail when tzinfo data for + any timezone defined in `ActiveSupport::TimeZone::MAPPING` is missing. - *Assain Jaleel* + *Dominik Sander* -* Make the order of `Hash#reverse_merge!` consistent with `HashWithIndifferentAccess`. +* Redis cache store: `delete_matched` no longer blocks the Redis server. + (Switches from evaled Lua to a batched SCAN + DEL loop.) - *Erol Fornoles* + *Gleb Mazovetskiy* -* Add `freeze_time` helper which freezes time to `Time.now` in tests. +* Fix bug where `ActiveSupport::Cache` will massively inflate the storage + size when compression is enabled (which is true by default). This patch + does not attempt to repair existing data: please manually flush the cache + to clear out the problematic entries. - *Prathamesh Sonpatki* + *Godfrey Chan* -* Default `ActiveSupport::MessageEncryptor` to use AES 256 GCM encryption. +* Fix bug where `URI.unescape` would fail with mixed Unicode/escaped character input: - On for new Rails 5.2 apps. Upgrading apps can find the config as a new - framework default. + URI.unescape("\xe3\x83\x90") # => "バ" + URI.unescape("%E3%83%90") # => "バ" + URI.unescape("\xe3\x83\x90%E3%83%90") # => Encoding::CompatibilityError - *Assain Jaleel* + *Ashe Connor*, *Aaron Patterson* -* Cache: `write_multi` +* Add `before?` and `after?` methods to `Date`, `DateTime`, + `Time`, and `TimeWithZone`. - Rails.cache.write_multi foo: 'bar', baz: 'qux' + *Nick Holden* - 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. +* `ActiveSupport::Inflector#ordinal` and `ActiveSupport::Inflector#ordinalize` now support + translations through I18n. - 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`. + # locale/fr.rb - *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`. + { + fr: { + number: { + nth: { + ordinals: lambda do |_key, number:, **_options| + if number.to_i.abs == 1 + 'er' + else + 'e' + end + end, - Before: + ordinalized: lambda do |_key, number:, **_options| + "#{number}#{ActiveSupport::Inflector.ordinal(number)}" + end + } + } + } + } - class_attribute :settings - self.settings = {} - Now: + *Christian Blais* - class_attribute :settings, default: {} +* Add `:private` option to ActiveSupport's `Module#delegate` + in order to delegate methods as private: - *DHH* + class User < ActiveRecord::Base + has_one :profile + delegate :date_of_birth, to: :profile, private: true -* `#singularize` and `#pluralize` now respect uncountables for the specified locale. + def age + Date.today.year - date_of_birth.year + end + end - *Eilis Hamilton* + # User.new.age # => 29 + # User.new.date_of_birth + # => NoMethodError: private method `date_of_birth' called for #<User:0x00000008221340> -* 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. + *Tomas Valent* - *DHH* +* `String#truncate_bytes` to truncate a string to a maximum bytesize without + breaking multibyte characters or grapheme clusters like 👩👩👦👦. -* 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* + *Jeremy Daer* -* 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. +* `String#strip_heredoc` preserves frozenness. - *DHH* + "foo".freeze.strip_heredoc.frozen? # => true -* Pass gem name and deprecation horizon to deprecation notifications. + Fixes that frozen string literals would inadvertently become unfrozen: - *Willem van Bergen* + # frozen_string_literal: true -* Add support for `:offset` and `:zone` to `ActiveSupport::TimeWithZone#change` + foo = <<-MSG.strip_heredoc + la la la + MSG - *Andrew White* + foo.frozen? # => false !?? -* Add support for `:offset` to `Time#change` + *Jeremy Daer* - Fixes #28723. +* Rails 6 requires Ruby 2.5.0 or newer. - *Andrew White* + *Jeremy Daer*, *Kasper Timm Hansen* -* Add `fetch_values` for `HashWithIndifferentAccess` +* Adds parallel testing to Rails. - The method was originally added to `Hash` in Ruby 2.3.0. + Parallelize your test suite with forked processes or threads. - *Josh Pencheon* + *Eileen M. Uchitelle*, *Aaron Patterson* -Please check [5-1-stable](https://github.com/rails/rails/blob/5-1-stable/activesupport/CHANGELOG.md) for previous changes. +Please check [5-2-stable](https://github.com/rails/rails/blob/5-2-stable/activesupport/CHANGELOG.md) for previous changes. diff --git a/activesupport/MIT-LICENSE b/activesupport/MIT-LICENSE index 6b3cead1a7..315a4bc7c4 100644 --- a/activesupport/MIT-LICENSE +++ b/activesupport/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2005-2017 David Heinemeier Hansson +Copyright (c) 2005-2019 David Heinemeier Hansson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/activesupport/Rakefile b/activesupport/Rakefile index 8672ab1542..f10f19be0a 100644 --- a/activesupport/Rakefile +++ b/activesupport/Rakefile @@ -14,10 +14,30 @@ Rake::TestTask.new do |t| t.ruby_opts = ["--dev"] if defined?(JRUBY_VERSION) end +Rake::Task[:test].enhance do + Rake::Task["test:cache_stores:redis:ruby"].invoke +end + namespace :test do task :isolated do Dir.glob("test/**/*_test.rb").all? do |file| sh(Gem.ruby, "-w", "-Ilib:test", file) end || raise("Failures") end + + namespace :cache_stores do + namespace :redis do + %w[ ruby hiredis ].each do |driver| + task("env:#{driver}") { ENV["REDIS_DRIVER"] = driver } + + Rake::TestTask.new(driver => "env:#{driver}") do |t| + t.libs << "test" + t.test_files = ["test/cache/stores/redis_cache_store_test.rb"] + t.warning = true + t.verbose = true + t.ruby_opts = ["--dev"] if defined?(JRUBY_VERSION) + end + end + end + end end diff --git a/activesupport/activesupport.gemspec b/activesupport/activesupport.gemspec index db801d2da2..2fa0623a9c 100644 --- a/activesupport/activesupport.gemspec +++ b/activesupport/activesupport.gemspec @@ -9,7 +9,7 @@ Gem::Specification.new do |s| s.summary = "A toolkit of support libraries and Ruby core extensions extracted from the Rails framework." s.description = "A toolkit of support libraries and Ruby core extensions extracted from the Rails framework. Rich support for multibyte strings, internationalization, time zones, and testing." - s.required_ruby_version = ">= 2.2.2" + s.required_ruby_version = ">= 2.5.0" s.license = "MIT" @@ -27,8 +27,12 @@ Gem::Specification.new do |s| "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" + # 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 "concurrent-ruby", "~> 1.0", ">= 1.0.2" + s.add_dependency "zeitwerk", "~> 1.2" end diff --git a/activesupport/bin/generate_tables b/activesupport/bin/generate_tables deleted file mode 100755 index 18199b2171..0000000000 --- a/activesupport/bin/generate_tables +++ /dev/null @@ -1,141 +0,0 @@ -#!/usr/bin/env ruby -# frozen_string_literal: true - -begin - $:.unshift(File.expand_path("../lib", __dir__)) - require "active_support" -rescue IOError -end - -require "open-uri" -require "tmpdir" -require "fileutils" - -module ActiveSupport - module Multibyte - module Unicode - class UnicodeDatabase - def load; end - end - - class DatabaseGenerator - BASE_URI = "http://www.unicode.org/Public/#{UNICODE_VERSION}/ucd/" - SOURCES = { - codepoints: BASE_URI + "UnicodeData.txt", - composition_exclusion: BASE_URI + "CompositionExclusions.txt", - grapheme_break_property: BASE_URI + "auxiliary/GraphemeBreakProperty.txt", - cp1252: "http://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP1252.TXT" - } - - def initialize - @ucd = Unicode::UnicodeDatabase.new - end - - def parse_codepoints(line) - codepoint = Codepoint.new - raise "Could not parse input." unless line =~ /^ - ([0-9A-F]+); # code - ([^;]+); # name - ([A-Z]+); # general category - ([0-9]+); # canonical combining class - ([A-Z]+); # bidi class - (<([A-Z]*)>)? # decomposition type - ((\ ?[0-9A-F]+)*); # decomposition mapping - ([0-9]*); # decimal digit - ([0-9]*); # digit - ([^;]*); # numeric - ([YN]*); # bidi mirrored - ([^;]*); # unicode 1.0 name - ([^;]*); # iso comment - ([0-9A-F]*); # simple uppercase mapping - ([0-9A-F]*); # simple lowercase mapping - ([0-9A-F]*)$/ix # simple titlecase mapping - codepoint.code = $1.hex - codepoint.combining_class = Integer($4) - codepoint.decomp_type = $7 - codepoint.decomp_mapping = ($8 == "") ? nil : $8.split.collect(&:hex) - codepoint.uppercase_mapping = ($16 == "") ? 0 : $16.hex - codepoint.lowercase_mapping = ($17 == "") ? 0 : $17.hex - @ucd.codepoints[codepoint.code] = codepoint - end - - def parse_grapheme_break_property(line) - if line =~ /^([0-9A-F.]+)\s*;\s*([\w]+)\s*#/ - type = $2.downcase.intern - @ucd.boundary[type] ||= [] - if $1.include? ".." - parts = $1.split ".." - @ucd.boundary[type] << (parts[0].hex..parts[1].hex) - else - @ucd.boundary[type] << $1.hex - end - end - end - - def parse_composition_exclusion(line) - if line =~ /^([0-9A-F]+)/i - @ucd.composition_exclusion << $1.hex - end - end - - def parse_cp1252(line) - if line =~ /^([0-9A-Fx]+)\s([0-9A-Fx]+)/i - @ucd.cp1252[$1.hex] = $2.hex - end - end - - def create_composition_map - @ucd.codepoints.each do |_, cp| - if !cp.nil? && cp.combining_class == 0 && cp.decomp_type.nil? && !cp.decomp_mapping.nil? && cp.decomp_mapping.length == 2 && @ucd.codepoints[cp.decomp_mapping[0]].combining_class == 0 && !@ucd.composition_exclusion.include?(cp.code) - @ucd.composition_map[cp.decomp_mapping[0]] ||= {} - @ucd.composition_map[cp.decomp_mapping[0]][cp.decomp_mapping[1]] = cp.code - end - end - end - - def normalize_boundary_map - @ucd.boundary.each do |k, v| - if [:lf, :cr].include? k - @ucd.boundary[k] = v[0] - end - end - end - - def parse - SOURCES.each do |type, url| - filename = File.join(Dir.tmpdir, UNICODE_VERSION, "#{url.split('/').last}") - unless File.exist?(filename) - $stderr.puts "Downloading #{url.split('/').last}" - FileUtils.mkdir_p(File.dirname(filename)) - File.open(filename, "wb") do |target| - open(url) do |source| - source.each_line { |line| target.write line } - end - end - end - File.open(filename) do |file| - file.each_line { |line| send "parse_#{type}".intern, line } - end - end - create_composition_map - normalize_boundary_map - end - - def dump_to(filename) - File.open(filename, "wb") do |f| - f.write Marshal.dump([@ucd.codepoints, @ucd.composition_exclusion, @ucd.composition_map, @ucd.boundary, @ucd.cp1252]) - end - end - end - end - end -end - -if __FILE__ == $0 - filename = ActiveSupport::Multibyte::Unicode::UnicodeDatabase.filename - generator = ActiveSupport::Multibyte::Unicode::DatabaseGenerator.new - generator.parse - print "Writing to: #{filename}" - generator.dump_to filename - puts " (#{File.size(filename)} bytes)" -end diff --git a/activesupport/lib/active_support.rb b/activesupport/lib/active_support.rb index 16b9a5bc1d..5589c71281 100644 --- a/activesupport/lib/active_support.rb +++ b/activesupport/lib/active_support.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true #-- -# Copyright (c) 2005-2017 David Heinemeier Hansson +# Copyright (c) 2005-2019 David Heinemeier Hansson # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/activesupport/lib/active_support/backtrace_cleaner.rb b/activesupport/lib/active_support/backtrace_cleaner.rb index 16dd733ddb..62973eca58 100644 --- a/activesupport/lib/active_support/backtrace_cleaner.rb +++ b/activesupport/lib/active_support/backtrace_cleaner.rb @@ -31,6 +31,9 @@ module ActiveSupport class BacktraceCleaner def initialize @filters, @silencers = [], [] + add_gem_filter + add_gem_silencer + add_stdlib_silencer end # Returns the backtrace after all filters and silencers have been run @@ -82,6 +85,26 @@ module ActiveSupport end private + + FORMATTED_GEMS_PATTERN = /\A[^\/]+ \([\w.]+\) / + + def add_gem_filter + gems_paths = (Gem.path | [Gem.default_dir]).map { |p| Regexp.escape(p) } + return if gems_paths.empty? + + gems_regexp = %r{(#{gems_paths.join('|')})/(bundler/)?gems/([^/]+)-([\w.]+)/(.*)} + gems_result = '\3 (\4) \5' + add_filter { |line| line.sub(gems_regexp, gems_result) } + end + + def add_gem_silencer + add_silencer { |line| FORMATTED_GEMS_PATTERN.match?(line) } + end + + def add_stdlib_silencer + add_silencer { |line| line.start_with?(RbConfig::CONFIG["rubylibdir"]) } + end + def filter_backtrace(backtrace) @filters.each do |f| backtrace = backtrace.map { |line| f.call(line) } diff --git a/activesupport/lib/active_support/cache.rb b/activesupport/lib/active_support/cache.rb index 395aa5e8f1..30a69c550b 100644 --- a/activesupport/lib/active_support/cache.rb +++ b/activesupport/lib/active_support/cache.rb @@ -91,16 +91,11 @@ module ActiveSupport private def retrieve_cache_key(key) case - when key.respond_to?(:cache_key_with_version) - key.cache_key_with_version - when key.respond_to?(:cache_key) - key.cache_key - when key.is_a?(Hash) - key.sort_by { |k, _| k.to_s }.collect { |k, v| "#{k}=#{v}" }.to_param - when key.respond_to?(:to_a) - key.to_a.collect { |element| retrieve_cache_key(element) }.to_param - 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 @@ -165,6 +160,23 @@ module ActiveSupport attr_reader :silence, :options alias :silence? :silence + class << self + private + def retrieve_pool_options(options) + {}.tap do |pool_options| + pool_options[:size] = options.delete(:pool_size) if options[:pool_size] + pool_options[:timeout] = options.delete(:pool_timeout) if options[:pool_timeout] + end + end + + def ensure_connection_pool_added! + require "connection_pool" + rescue LoadError => e + $stderr.puts "You don't have connection_pool installed in your application. Please add it to your Gemfile and run bundle install" + raise e + end + end + # Creates a new cache. The options will be passed to any write method calls # except for <tt>:namespace</tt> which can be used to set the global # namespace for the cache. @@ -217,6 +229,14 @@ module ActiveSupport # ask whether you should force a cache write. Otherwise, it's clearer to # just call <tt>Cache#write</tt>. # + # Setting <tt>skip_nil: true</tt> will not cache nil result: + # + # cache.fetch('foo') { nil } + # cache.fetch('bar', skip_nil: true) { nil } + # cache.exist?('foo') # => true + # cache.exist?('bar') # => false + # + # # Setting <tt>compress: false</tt> disables compression of the cache entry. # # Setting <tt>:expires_in</tt> will set an expiration time on the cache. @@ -321,8 +341,9 @@ module ActiveSupport # 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. + # 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) @@ -362,23 +383,11 @@ module ActiveSupport options = names.extract_options! options = merged_options(options) - results = {} - names.each do |name| - 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 + instrument :read_multi, names, options do |payload| + read_multi_entries(names, options).tap do |results| + payload[:hits] = results.keys end end - results end # Cache Storage API to write multiple values at once. @@ -402,8 +411,6 @@ module ActiveSupport # to the cache. If you do not want to write the cache when the cache is # not found, use #read_multi. # - # Options are passed to the underlying cache implementation. - # # Returns a hash with the data for each of the names. For example: # # cache.write("bim", "bam") @@ -413,20 +420,36 @@ module ActiveSupport # # => { "bim" => "bam", # # "unknown_key" => "Fallback value for key: unknown_key" } # + # Options are passed to the underlying cache implementation. For example: + # + # cache.fetch_multi("fizz", expires_in: 5.seconds) do |key| + # "buzz" + # end + # # => {"fizz"=>"buzz"} + # cache.read("fizz") + # # => "buzz" + # sleep(6) + # cache.read("fizz") + # # => nil def fetch_multi(*names) raise ArgumentError, "Missing block: `Cache#fetch_multi` requires a block." unless block_given? options = names.extract_options! options = merged_options(options) - read_multi(*names, options).tap do |results| - writes = {} - - (names - results.keys).each do |name| - results[name] = writes[name] = yield(name) + instrument :read_multi, names, options do |payload| + reads = read_multi_entries(names, options) + writes = {} + ordered = names.each_with_object({}) do |name, hash| + hash[name] = reads.fetch(name) { writes[name] = yield(name) } end - write_multi writes, options + payload[:hits] = reads.keys + payload[:super_operation] = :fetch_multi + + write_multi(writes, options) + + ordered end end @@ -543,6 +566,28 @@ module ActiveSupport raise NotImplementedError.new end + # Reads multiple entries from the cache implementation. Subclasses MAY + # implement this method. + def read_multi_entries(names, options) + results = {} + names.each do |name| + 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 + end + end + results + end + # Writes multiple entries to the cache implementation. Subclasses MAY # implement this method. def write_multi_entries(hash, options) @@ -560,16 +605,20 @@ module ActiveSupport # Merges the default options with ones specific to a method call. def merged_options(call_options) if call_options - options.merge(call_options) + if options.empty? + call_options + else + options.merge(call_options) + end else - options.dup + options end end # Expands and namespaces the cache key. May be overridden by # cache stores to do additional normalization. def normalize_key(key, options = nil) - namespace_key Cache.expand_cache_key(key), options + namespace_key expanded_key(key), options end # Prefix the key with a namespace string: @@ -596,6 +645,26 @@ module ActiveSupport end 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. + def expanded_key(key) + return key.cache_key.to_s if key.respond_to?(:cache_key) + + case key + when Array + if key.size > 1 + key = key.collect { |element| expanded_key(element) } + else + key = expanded_key(key.first) + end + when Hash + key = key.sort_by { |k, _| k.to_s }.collect { |k, v| "#{k}=#{v}" } + end + + key.to_param + end + def normalize_version(key, options = nil) (options && options[:version].try(:to_param)) || expanded_version(key) end @@ -638,7 +707,7 @@ module ActiveSupport end def get_entry_value(entry, name, options) - instrument(:fetch_hit, name, options) {} + instrument(:fetch_hit, name, options) { } entry.value end @@ -647,7 +716,7 @@ module ActiveSupport yield(name) end - write(name, result, options) + write(name, result, options) unless result.nil? && options[:skip_nil] result end end @@ -665,19 +734,14 @@ module ActiveSupport DEFAULT_COMPRESS_LIMIT = 1.kilobyte # Creates a new cache entry for the specified value. Options supported are - # +:compress+, +:compress_threshold+, and +:expires_in+. - def initialize(value, options = {}) - if should_compress?(value, options) - @value = compress(value) - @compressed = true - else - @value = value - end - - @version = options[:version] + # +:compress+, +:compress_threshold+, +:version+ and +:expires_in+. + def initialize(value, compress: true, compress_threshold: DEFAULT_COMPRESS_LIMIT, version: nil, expires_in: nil, **) + @value = value + @version = version @created_at = Time.now.to_f - @expires_in = options[:expires_in] - @expires_in = @expires_in.to_f if @expires_in + @expires_in = expires_in && expires_in.to_f + + compress!(compress_threshold) if compress end def value @@ -709,17 +773,13 @@ module ActiveSupport # Returns the size of the cached value. This could be less than # <tt>value.size</tt> if the data is compressed. def size - if defined?(@s) - @s + case value + when NilClass + 0 + when String + @value.bytesize else - case value - when NilClass - 0 - when String - @value.bytesize - else - @s = Marshal.dump(@value).bytesize - end + @s ||= Marshal.dump(@value).bytesize end end @@ -736,23 +796,30 @@ module ActiveSupport end private - def should_compress?(value, options) - if value && options.fetch(:compress, true) - compress_threshold = options.fetch(:compress_threshold, DEFAULT_COMPRESS_LIMIT) - serialized_value_size = (value.is_a?(String) ? value : Marshal.dump(value)).bytesize - - return true if serialized_value_size >= compress_threshold + def compress!(compress_threshold) + case @value + when nil, true, false, Numeric + uncompressed_size = 0 + when String + uncompressed_size = @value.bytesize + else + serialized = Marshal.dump(@value) + uncompressed_size = serialized.bytesize end - false - end + if uncompressed_size >= compress_threshold + serialized ||= Marshal.dump(@value) + compressed = Zlib::Deflate.deflate(serialized) - def compressed? - defined?(@compressed) ? @compressed : false + if compressed.bytesize < uncompressed_size + @value = compressed + @compressed = true + end + end end - def compress(value) - Zlib::Deflate.deflate(Marshal.dump(value)) + def compressed? + defined?(@compressed) end def uncompress(value) diff --git a/activesupport/lib/active_support/cache/file_store.rb b/activesupport/lib/active_support/cache/file_store.rb index a0f44aac0f..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) @@ -26,11 +25,16 @@ module ActiveSupport @cache_path = cache_path.to_s end + # Advertise cache versioning support. + def self.supports_cache_versioning? + true + end + # 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(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 @@ -103,12 +107,10 @@ module ActiveSupport def lock_file(file_name, &block) if File.exist?(file_name) File.open(file_name, "r+") do |f| - begin - f.flock File::LOCK_EX - yield - ensure - f.flock File::LOCK_UN - end + f.flock File::LOCK_EX + yield + ensure + f.flock File::LOCK_UN end else yield @@ -127,15 +129,19 @@ module ActiveSupport hash = Zlib.adler32(fname) hash, dir_1 = hash.divmod(0x1000) dir_2 = hash.modulo(0x1000) - fname_paths = [] # Make sure file name doesn't exceed file system limits. - begin - fname_paths << fname[0, FILENAME_MAX_SIZE] - fname = fname[FILENAME_MAX_SIZE..-1] - end until fname.blank? + if fname.length < FILENAME_MAX_SIZE + fname_paths = fname + else + fname_paths = [] + begin + fname_paths << fname[0, FILENAME_MAX_SIZE] + fname = fname[FILENAME_MAX_SIZE..-1] + end until fname.blank? + end - File.join(cache_path, DIR_FORMATTER % dir_1, DIR_FORMATTER % dir_2, *fname_paths) + File.join(cache_path, DIR_FORMATTER % dir_1, DIR_FORMATTER % dir_2, fname_paths) end # Translate a file path into a key. @@ -147,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 @@ -160,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) @@ -186,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/mem_cache_store.rb b/activesupport/lib/active_support/cache/mem_cache_store.rb index df8bc8e43e..174c784deb 100644 --- a/activesupport/lib/active_support/cache/mem_cache_store.rb +++ b/activesupport/lib/active_support/cache/mem_cache_store.rb @@ -47,6 +47,11 @@ module ActiveSupport end end + # Advertise cache versioning support. + def self.supports_cache_versioning? + true + end + prepend Strategy::LocalCache prepend LocalCacheWithRaw @@ -63,7 +68,14 @@ module ActiveSupport addresses = addresses.flatten options = addresses.extract_options! addresses = ["localhost:11211"] if addresses.empty? - Dalli::Client.new(addresses, options) + pool_options = retrieve_pool_options(options) + + if pool_options.empty? + Dalli::Client.new(addresses, options) + else + ensure_connection_pool_added! + ConnectionPool.new(pool_options) { Dalli::Client.new(addresses, options.merge(threadsafe: false)) } + end end # Creates a new MemCacheStore object, with the given memcached server @@ -91,28 +103,6 @@ module ActiveSupport end end - # Reads multiple values from the cache using a single call to the - # servers for all keys. Options can be passed in the last argument. - def read_multi(*names) - options = names.extract_options! - options = merged_options(options) - - keys_to_names = 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) - - unless entry.expired? || entry.mismatched?(normalize_version(keys_to_names[key], options)) - values[keys_to_names[key]] = entry.value - end - end - - values - end - # Increment a cached value. This method uses the memcached incr atomic # operator and can only be used on values written with the :raw option. # Calling it on a value not stored with :raw will initialize that value @@ -121,7 +111,7 @@ module ActiveSupport options = merged_options(options) instrument(:increment, name, amount: amount) do rescue_error_with nil do - @data.incr(normalize_key(name, options), amount, options[:expires_in]) + @data.with { |c| c.incr(normalize_key(name, options), amount, options[:expires_in]) } end end end @@ -134,7 +124,7 @@ module ActiveSupport options = merged_options(options) instrument(:decrement, name, amount: amount) do rescue_error_with nil do - @data.decr(normalize_key(name, options), amount, options[:expires_in]) + @data.with { |c| c.decr(normalize_key(name, options), amount, options[:expires_in]) } end end end @@ -142,18 +132,18 @@ module ActiveSupport # Clear the entire cache on all memcached servers. This method should # be used with care when shared cache is being used. def clear(options = nil) - rescue_error_with(nil) { @data.flush_all } + rescue_error_with(nil) { @data.with { |c| c.flush_all } } end # Get the statistics from the memcached servers. def stats - @data.stats + @data.with { |c| c.stats } end private # Read an entry from the cache. def read_entry(key, options) - rescue_error_with(nil) { deserialize_entry(@data.get(key, options)) } + rescue_error_with(nil) { deserialize_entry(@data.with { |c| c.get(key, options) }) } end # Write an entry to the cache. @@ -166,13 +156,31 @@ module ActiveSupport expires_in += 5.minutes end rescue_error_with false do - @data.send(method, key, value, expires_in, options) + @data.with { |c| c.send(method, key, value, expires_in, options) } end end + # Reads multiple entries from the cache implementation. + def read_multi_entries(names, options) + keys_to_names = Hash[names.map { |name| [normalize_key(name, options), name] }] + + raw_values = @data.with { |c| c.get_multi(keys_to_names.keys) } + values = {} + + raw_values.each do |key, value| + entry = deserialize_entry(value) + + unless entry.expired? || entry.mismatched?(normalize_version(keys_to_names[key], options)) + values[keys_to_names[key]] = entry.value + end + end + + values + end + # Delete an entry from the cache. def delete_entry(key, options) - rescue_error_with(false) { @data.delete(key) } + rescue_error_with(false) { @data.with { |c| c.delete(key) } } end # Memcache keys are binaries. So we need to force their encoding to binary diff --git a/activesupport/lib/active_support/cache/memory_store.rb b/activesupport/lib/active_support/cache/memory_store.rb index 564ac17241..629eb2dd70 100644 --- a/activesupport/lib/active_support/cache/memory_store.rb +++ b/activesupport/lib/active_support/cache/memory_store.rb @@ -30,6 +30,11 @@ module ActiveSupport @pruning = false end + # Advertise cache versioning support. + def self.supports_cache_versioning? + true + end + # Delete all data stored in a given cache store. def clear(options = nil) synchronize do @@ -57,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/null_store.rb b/activesupport/lib/active_support/cache/null_store.rb index 1a5983db43..8452a28fd8 100644 --- a/activesupport/lib/active_support/cache/null_store.rb +++ b/activesupport/lib/active_support/cache/null_store.rb @@ -12,6 +12,11 @@ module ActiveSupport class NullStore < Store prepend Strategy::LocalCache + # Advertise cache versioning support. + def self.supports_cache_versioning? + true + end + def clear(options = nil) end diff --git a/activesupport/lib/active_support/cache/redis_cache_store.rb b/activesupport/lib/active_support/cache/redis_cache_store.rb index 6cc45f5284..9a55e49e27 100644 --- a/activesupport/lib/active_support/cache/redis_cache_store.rb +++ b/activesupport/lib/active_support/cache/redis_cache_store.rb @@ -20,6 +20,15 @@ require "active_support/core_ext/marshal" module ActiveSupport module Cache + module ConnectionPoolLike + def with + yield self + end + end + + ::Redis.include(ConnectionPoolLike) + ::Redis::Distributed.include(ConnectionPoolLike) + # Redis cache store. # # Deployment note: Take care to use a *dedicated Redis cache* rather @@ -53,8 +62,14 @@ module ActiveSupport end end - DELETE_GLOB_LUA = "for i, name in ipairs(redis.call('KEYS', ARGV[1])) do redis.call('DEL', name); end" - private_constant :DELETE_GLOB_LUA + # The maximum number of entries to receive per SCAN call. + SCAN_BATCH_SIZE = 1000 + private_constant :SCAN_BATCH_SIZE + + # Advertise cache versioning support. + def self.supports_cache_versioning? + true + end # Support raw values in the local cache strategy. module LocalCacheWithRaw # :nodoc: @@ -69,7 +84,7 @@ module ActiveSupport def write_entry(key, entry, options) if options[:raw] && local_cache - raw_entry = Entry.new(entry.value.to_s) + raw_entry = Entry.new(serialize_entry(entry, raw: true)) raw_entry.expires_at = entry.expires_at super(key, raw_entry, options) else @@ -80,7 +95,7 @@ module ActiveSupport def write_multi_entries(entries, options) if options[:raw] && local_cache raw_entries = entries.map do |key, entry| - raw_entry = Entry.new(entry.value.to_s) + raw_entry = Entry.new(serialize_entry(entry, raw: true)) raw_entry.expires_at = entry.expires_at end.to_h @@ -109,7 +124,7 @@ module ActiveSupport def build_redis(redis: nil, url: nil, **redis_options) #:nodoc: urls = Array(url) - if redis.respond_to?(:call) + if redis.is_a?(Proc) redis.call elsif redis redis @@ -145,11 +160,11 @@ module ActiveSupport # :url Array -> Redis::Distributed.new([{ url: … }, { url: … }, …]) # # No namespace is set by default. Provide one if the Redis cache - # server is shared with other apps: <tt>namespace: 'myapp-cache'<tt>. + # server is shared with other apps: <tt>namespace: 'myapp-cache'</tt>. # # Compression is enabled by default with a 1kB threshold, so cached # values larger than 1kB are automatically compressed. Disable by - # passing <tt>cache: false</tt> or change the threshold by passing + # passing <tt>compress: false</tt> or change the threshold by passing # <tt>compress_threshold: 4.kilobytes</tt>. # # No expiry is set on cache entries by default. Redis is expected to @@ -172,7 +187,16 @@ module ActiveSupport end def redis - @redis ||= self.class.build_redis(**redis_options) + @redis ||= begin + pool_options = self.class.send(:retrieve_pool_options, redis_options) + + if pool_options.any? + self.class.send(:ensure_connection_pool_added!) + ::ConnectionPool.new(pool_options) { self.class.build_redis(**redis_options) } + else + self.class.build_redis(**redis_options) + end + end end def inspect @@ -186,7 +210,11 @@ module ActiveSupport # fetched values. def read_multi(*names) if mget_capable? - read_multi_mget(*names) + instrument(:read_multi, names, options) do |payload| + read_multi_mget(*names).tap do |results| + payload[:hits] = results.keys + end + end else super end @@ -209,12 +237,18 @@ module ActiveSupport # Failsafe: Raises errors. def delete_matched(matcher, options = nil) instrument :delete_matched, matcher do - case matcher - when String - redis.eval DELETE_GLOB_LUA, [], [namespace_key(matcher, options)] - else + unless String === matcher raise ArgumentError, "Only Redis glob strings are supported: #{matcher.inspect}" end + redis.with do |c| + pattern = namespace_key(matcher, options) + cursor = "0" + # Fetch keys in batches using SCAN to avoid blocking the Redis server. + begin + cursor, keys = c.scan(cursor, match: pattern, count: SCAN_BATCH_SIZE) + c.del(*keys) unless keys.empty? + end until cursor == "0" + end end end @@ -228,7 +262,16 @@ module ActiveSupport # Failsafe: Raises errors. def increment(name, amount = 1, options = nil) instrument :increment, name, amount: amount do - redis.incrby normalize_key(name, options), amount + failsafe :increment do + options = merged_options(options) + key = normalize_key(name, options) + + redis.with do |c| + c.incrby(key, amount).tap do + write_key_expiry(c, key, options) + end + end + end end end @@ -242,7 +285,16 @@ module ActiveSupport # Failsafe: Raises errors. def decrement(name, amount = 1, options = nil) instrument :decrement, name, amount: amount do - redis.decrby normalize_key(name, options), amount + failsafe :decrement do + options = merged_options(options) + key = normalize_key(name, options) + + redis.with do |c| + c.decrby(key, amount).tap do + write_key_expiry(c, key, options) + end + end + end end end @@ -260,10 +312,10 @@ module ActiveSupport # Failsafe: Raises errors. def clear(options = nil) failsafe :clear do - if namespace = merged_options(options)[namespace] + if namespace = merged_options(options)[:namespace] delete_matched "*", namespace: namespace else - redis.flushdb + redis.with { |c| c.flushdb } end end end @@ -294,7 +346,15 @@ module ActiveSupport # Read an entry from the cache. def read_entry(key, options = nil) failsafe :read_entry do - deserialize_entry redis.get(key) + deserialize_entry redis.with { |c| c.get(key) } + end + end + + def read_multi_entries(names, _options) + if mget_capable? + read_multi_mget(*names) + else + super end end @@ -303,7 +363,10 @@ module ActiveSupport options = merged_options(options) keys = names.map { |name| normalize_key(name, options) } - values = redis.mget(*keys) + + values = failsafe(:read_multi_mget, returning: {}) do + redis.with { |c| c.mget(*keys) } + end names.zip(values).each_with_object({}) do |(name, value), results| if value @@ -319,7 +382,7 @@ module ActiveSupport # # Requires Redis 2.6.12+ for extended SET options. def write_entry(key, entry, unless_exist: false, raw: false, expires_in: nil, race_condition_ttl: nil, **options) - value = raw ? entry.value.to_s : serialize_entry(entry) + serialized_entry = serialize_entry(entry, raw: raw) # If race condition TTL is in use, ensure that cache entries # stick around a bit longer after they would have expired @@ -328,23 +391,29 @@ module ActiveSupport expires_in += 5.minutes end - failsafe :write_entry do + failsafe :write_entry, returning: false do if unless_exist || expires_in modifiers = {} modifiers[:nx] = unless_exist modifiers[:px] = (1000 * expires_in.to_f).ceil if expires_in - redis.set key, value, modifiers + redis.with { |c| c.set key, serialized_entry, modifiers } else - redis.set key, value + redis.with { |c| c.set key, serialized_entry } end end end + def write_key_expiry(client, key, options) + if options[:expires_in] && client.ttl(key).negative? + client.expire key, options[:expires_in].to_i + end + end + # Delete an entry from the cache. def delete_entry(key, options) failsafe :delete_entry, returning: false do - redis.del key + redis.with { |c| c.del key } end end @@ -353,7 +422,7 @@ module ActiveSupport if entries.any? if mset_capable? && expires_in.nil? failsafe :write_multi_entries do - redis.mapped_mset(entries) + redis.with { |c| c.mapped_mset(serialize_entries(entries, raw: options[:raw])) } end else super @@ -363,12 +432,12 @@ module ActiveSupport # Truncate keys that exceed 1kB. def normalize_key(key, options) - truncate_key super + truncate_key super.b end def truncate_key(key) if key.bytesize > max_key_bytesize - suffix = ":sha2:#{Digest::SHA2.hexdigest(key)}" + suffix = ":sha2:#{::Digest::SHA2.hexdigest(key)}" truncate_at = max_key_bytesize - suffix.bytesize "#{key.byteslice(0, truncate_at)}#{suffix}" else @@ -376,15 +445,25 @@ module ActiveSupport end end - def deserialize_entry(raw_value) - if raw_value - entry = Marshal.load(raw_value) rescue raw_value + def deserialize_entry(serialized_entry) + if serialized_entry + entry = Marshal.load(serialized_entry) rescue serialized_entry entry.is_a?(Entry) ? entry : Entry.new(entry) end end - def serialize_entry(entry) - Marshal.dump(entry) + def serialize_entry(entry, raw: false) + if raw + entry.value.to_s + else + Marshal.dump(entry) + end + end + + def serialize_entries(entries, raw: false) + entries.transform_values do |entry| + serialize_entry entry, raw: raw + end end def failsafe(method, returning: nil) diff --git a/activesupport/lib/active_support/cache/strategy/local_cache.rb b/activesupport/lib/active_support/cache/strategy/local_cache.rb index aaa9638fa8..39b32fc7f6 100644 --- a/activesupport/lib/active_support/cache/strategy/local_cache.rb +++ b/activesupport/lib/active_support/cache/strategy/local_cache.rb @@ -54,6 +54,17 @@ module ActiveSupport @data[key] end + def read_multi_entries(keys, options) + values = {} + + keys.each do |name| + entry = read_entry(name, options) + values[name] = entry.value if entry + end + + values + end + def write_entry(key, value, options) @data[key] = value true @@ -116,6 +127,19 @@ module ActiveSupport end end + def read_multi_entries(keys, options) + return super unless local_cache + + local_entries = local_cache.read_multi_entries(keys, options) + missed_keys = keys - local_entries.keys + + if missed_keys.any? + local_entries.merge!(super(missed_keys, options)) + else + local_entries + end + end + def write_entry(key, entry, options) if options[:unless_exist] local_cache.delete_entry(key, options) if local_cache diff --git a/activesupport/lib/active_support/callbacks.rb b/activesupport/lib/active_support/callbacks.rb index 0ed4681b7d..d0644a0f7e 100644 --- a/activesupport/lib/active_support/callbacks.rb +++ b/activesupport/lib/active_support/callbacks.rb @@ -23,6 +23,9 @@ module ActiveSupport # +ClassMethods.set_callback+), and run the installed callbacks at the # appropriate times (via +run_callbacks+). # + # By default callbacks are halted by throwing +:abort+. + # See +ClassMethods.define_callbacks+ for details. + # # Three kinds of callbacks are supported: before callbacks, run before a # certain event; after callbacks, run after the event; and around callbacks, # blocks that surround the event, triggering it when they yield. Callback code @@ -497,9 +500,7 @@ module ActiveSupport arg.halted || !@user_conditions.all? { |c| c.call(arg.target, arg.value) } end - def nested - @nested - end + attr_reader :nested def final? !@call_template @@ -578,7 +579,7 @@ module ActiveSupport end protected - def chain; @chain; end + attr_reader :chain private @@ -659,9 +660,17 @@ module ActiveSupport # * <tt>:if</tt> - A symbol or an array of symbols, each naming an instance # method or a proc; the callback will be called only when they all return # a true value. + # + # If a proc is given, its body is evaluated in the context of the + # current object. It can also optionally accept the current object as + # an argument. # * <tt>:unless</tt> - A symbol or an array of symbols, each naming an # instance method or a proc; the callback will be called only when they # all return a false value. + # + # If a proc is given, its body is evaluated in the context of the + # current object. It can also optionally accept the current object as + # an argument. # * <tt>:prepend</tt> - If +true+, the callback will be prepended to the # existing chain rather than appended. def set_callback(name, *filter_list, &block) @@ -749,8 +758,8 @@ module ActiveSupport # * <tt>:skip_after_callbacks_if_terminated</tt> - Determines if after # callbacks should be terminated by the <tt>:terminator</tt> option. By # default after callbacks are executed no matter if callback chain was - # terminated or not. This option makes sense only when <tt>:terminator</tt> - # option is specified. + # terminated or not. This option has no effect if <tt>:terminator</tt> + # option is set to +nil+. # # * <tt>:scope</tt> - Indicates which methods should be executed when an # object is used as a callback. @@ -809,7 +818,9 @@ module ActiveSupport names.each do |name| name = name.to_sym - set_callbacks name, CallbackChain.new(name, options) + ([self] + ActiveSupport::DescendantsTracker.descendants(self)).each do |target| + target.set_callbacks name, CallbackChain.new(name, options) + end module_eval <<-RUBY, __FILE__, __LINE__ + 1 def _run_#{name}_callbacks(&block) diff --git a/activesupport/lib/active_support/concern.rb b/activesupport/lib/active_support/concern.rb index b0a0d845e5..5d356a0ab6 100644 --- a/activesupport/lib/active_support/concern.rb +++ b/activesupport/lib/active_support/concern.rb @@ -125,9 +125,13 @@ module ActiveSupport def included(base = nil, &block) if base.nil? - raise MultipleIncludedBlocks if instance_variable_defined?(:@_included_block) - - @_included_block = block + if instance_variable_defined?(:@_included_block) + if @_included_block.source_location != block.source_location + raise MultipleIncludedBlocks + end + else + @_included_block = block + end else super end diff --git a/activesupport/lib/active_support/configurable.rb b/activesupport/lib/active_support/configurable.rb index 4d6f7819bb..9acf674c40 100644 --- a/activesupport/lib/active_support/configurable.rb +++ b/activesupport/lib/active_support/configurable.rb @@ -2,8 +2,6 @@ require "active_support/concern" require "active_support/ordered_options" -require "active_support/core_ext/array/extract_options" -require "active_support/core_ext/regexp" module ActiveSupport # Configurable provides a <tt>config</tt> method to store and retrieve @@ -106,9 +104,7 @@ module ActiveSupport # end # # User.hair_colors # => [:brown, :black, :blonde, :red] - def config_accessor(*names) - options = names.extract_options! - + def config_accessor(*names, instance_reader: true, instance_writer: true, instance_accessor: true) # :doc: names.each do |name| raise NameError.new("invalid config attribute name") unless /\A[_A-Za-z]\w*\z/.match?(name) @@ -118,9 +114,9 @@ module ActiveSupport singleton_class.class_eval reader, __FILE__, reader_line singleton_class.class_eval writer, __FILE__, writer_line - unless options[:instance_accessor] == false - class_eval reader, __FILE__, reader_line unless options[:instance_reader] == false - class_eval writer, __FILE__, writer_line unless options[:instance_writer] == false + if instance_accessor + class_eval reader, __FILE__, reader_line if instance_reader + class_eval writer, __FILE__, writer_line if instance_writer end send("#{name}=", yield) if block_given? end diff --git a/activesupport/lib/active_support/core_ext/array.rb b/activesupport/lib/active_support/core_ext/array.rb index 6d83b76882..88b6567712 100644 --- a/activesupport/lib/active_support/core_ext/array.rb +++ b/activesupport/lib/active_support/core_ext/array.rb @@ -3,7 +3,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" 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" diff --git a/activesupport/lib/active_support/core_ext/array/extract.rb b/activesupport/lib/active_support/core_ext/array/extract.rb new file mode 100644 index 0000000000..cc5a8a3f88 --- /dev/null +++ b/activesupport/lib/active_support/core_ext/array/extract.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +class Array + # Removes and returns the elements for which the block returns a true value. + # If no block is given, an Enumerator is returned instead. + # + # numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + # odd_numbers = numbers.extract! { |number| number.odd? } # => [1, 3, 5, 7, 9] + # numbers # => [0, 2, 4, 6, 8] + def extract! + return to_enum(:extract!) { size } unless block_given? + + extracted_elements = [] + + reject! do |element| + extracted_elements << element if yield(element) + end + + extracted_elements + end +end 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 661971d7cd..ba3739f640 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,9 +1,5 @@ # frozen_string_literal: true -class Array - # The human way of thinking about adding stuff to the end of a list is with append. - alias_method :append, :push unless [].respond_to?(:append) +require "active_support/deprecation" - # The human way of thinking about adding stuff to the beginning of a list is with prepend. - alias_method :prepend, :unshift unless [].respond_to?(:prepend) -end +ActiveSupport::Deprecation.warn "Ruby 2.5+ (required by Rails 6) provides Array#append and Array#prepend natively, so requiring active_support/core_ext/array/prepend_and_append is no longer necessary. Requiring it will raise LoadError in Rails 6.1." diff --git a/activesupport/lib/active_support/core_ext/class/attribute.rb b/activesupport/lib/active_support/core_ext/class/attribute.rb index 7928efb871..fa33ff945f 100644 --- a/activesupport/lib/active_support/core_ext/class/attribute.rb +++ b/activesupport/lib/active_support/core_ext/class/attribute.rb @@ -98,7 +98,7 @@ class Class singleton_class.silence_redefinition_of_method("#{name}?") define_singleton_method("#{name}?") { !!public_send(name) } if instance_predicate - ivar = "@#{name}" + ivar = "@#{name}".to_sym singleton_class.silence_redefinition_of_method("#{name}=") define_singleton_method("#{name}=") do |val| diff --git a/activesupport/lib/active_support/core_ext/class/subclasses.rb b/activesupport/lib/active_support/core_ext/class/subclasses.rb index 75e65337b7..56fb46a88d 100644 --- a/activesupport/lib/active_support/core_ext/class/subclasses.rb +++ b/activesupport/lib/active_support/core_ext/class/subclasses.rb @@ -3,7 +3,7 @@ class Class begin # Test if this Ruby supports each_object against singleton_class - ObjectSpace.each_object(Numeric.singleton_class) {} + ObjectSpace.each_object(Numeric.singleton_class) { } # Returns an array with all classes that are < than its receiver. # diff --git a/activesupport/lib/active_support/core_ext/date/calculations.rb b/activesupport/lib/active_support/core_ext/date/calculations.rb index 1cd7acb05d..d03a8d3997 100644 --- a/activesupport/lib/active_support/core_ext/date/calculations.rb +++ b/activesupport/lib/active_support/core_ext/date/calculations.rb @@ -110,12 +110,13 @@ class Date # Provides precise Date calculations for years, months, and days. The +options+ parameter takes a hash with # any of these keys: <tt>:years</tt>, <tt>:months</tt>, <tt>:weeks</tt>, <tt>:days</tt>. def advance(options) - options = options.dup d = self - d = d >> options.delete(:years) * 12 if options[:years] - d = d >> options.delete(:months) if options[:months] - d = d + options.delete(:weeks) * 7 if options[:weeks] - d = d + options.delete(:days) if options[:days] + + d = d >> options[:years] * 12 if options[:years] + d = d >> options[:months] if options[:months] + d = d + options[:weeks] * 7 if options[:weeks] + d = d + options[:days] if options[:days] + d end diff --git a/activesupport/lib/active_support/core_ext/date_and_time/calculations.rb b/activesupport/lib/active_support/core_ext/date_and_time/calculations.rb index f6cb1a384c..e2e11545e2 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 @@ -5,13 +5,13 @@ require "active_support/core_ext/object/try" module DateAndTime module Calculations DAYS_INTO_WEEK = { - monday: 0, - tuesday: 1, - wednesday: 2, - thursday: 3, - friday: 4, - saturday: 5, - sunday: 6 + sunday: 0, + monday: 1, + tuesday: 2, + wednesday: 3, + thursday: 4, + friday: 5, + saturday: 6 } WEEKEND_DAYS = [ 6, 0 ] @@ -60,6 +60,16 @@ module DateAndTime !WEEKEND_DAYS.include?(wday) end + # Returns true if the date/time falls before <tt>date_or_time</tt>. + def before?(date_or_time) + self < date_or_time + end + + # Returns true if the date/time falls after <tt>date_or_time</tt>. + def after?(date_or_time) + self > date_or_time + end + # Returns a new date/time the specified number of days ago. def days_ago(days) advance(days: -days) @@ -124,7 +134,7 @@ module DateAndTime # now = DateTime.current # => Fri, 10 Jul 2015 18:41:29 +0000 # now.beginning_of_quarter # => Wed, 01 Jul 2015 00:00:00 +0000 def beginning_of_quarter - first_quarter_month = [10, 7, 4, 1].detect { |m| m <= month } + first_quarter_month = month - (2 + month) % 3 beginning_of_month.change(month: first_quarter_month) end alias :at_beginning_of_quarter :beginning_of_quarter @@ -139,7 +149,7 @@ module DateAndTime # now = DateTime.current # => Fri, 10 Jul 2015 18:41:29 +0000 # now.end_of_quarter # => Wed, 30 Sep 2015 23:59:59 +0000 def end_of_quarter - last_quarter_month = [3, 6, 9, 12].detect { |m| m >= month } + last_quarter_month = month + (12 - month) % 3 beginning_of_month.change(month: last_quarter_month).end_of_month end alias :at_end_of_quarter :end_of_quarter @@ -253,9 +263,8 @@ module DateAndTime # Week is assumed to start on +start_day+, default is # +Date.beginning_of_week+ or +config.beginning_of_week+ when set. def days_to_week_start(start_day = Date.beginning_of_week) - start_day_number = DAYS_INTO_WEEK[start_day] - current_day_number = wday != 0 ? wday - 1 : 6 - (current_day_number - start_day_number) % 7 + start_day_number = DAYS_INTO_WEEK.fetch(start_day) + (wday - start_day_number) % 7 end # Returns a new date/time representing the start of this week on the given day. @@ -336,8 +345,7 @@ module DateAndTime # today.next_occurring(:monday) # => Mon, 18 Dec 2017 # today.next_occurring(:thursday) # => Thu, 21 Dec 2017 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 = DAYS_INTO_WEEK.fetch(day_of_week) - wday from_now += 7 unless from_now > 0 advance(days: from_now) end @@ -348,8 +356,7 @@ module DateAndTime # today.prev_occurring(:monday) # => Mon, 11 Dec 2017 # today.prev_occurring(:thursday) # => Thu, 07 Dec 2017 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 = wday - DAYS_INTO_WEEK.fetch(day_of_week) ago += 7 unless ago > 0 advance(days: -ago) end @@ -364,7 +371,7 @@ module DateAndTime end def days_span(day) - (DAYS_INTO_WEEK[day] - DAYS_INTO_WEEK[Date.beginning_of_week]) % 7 + (DAYS_INTO_WEEK.fetch(day) - DAYS_INTO_WEEK.fetch(Date.beginning_of_week)) % 7 end def copy_time_to(other) diff --git a/activesupport/lib/active_support/core_ext/date_time/calculations.rb b/activesupport/lib/active_support/core_ext/date_time/calculations.rb index e61b23f842..bc670c3e76 100644 --- a/activesupport/lib/active_support/core_ext/date_time/calculations.rb +++ b/activesupport/lib/active_support/core_ext/date_time/calculations.rb @@ -110,7 +110,7 @@ class DateTime # instance time. Do not use this method in combination with x.months, use # months_since instead! def since(seconds) - self + Rational(seconds.round, 86400) + self + Rational(seconds, 86400) end alias :in :since 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 2d6b49722d..7600a067cc 100644 --- a/activesupport/lib/active_support/core_ext/date_time/compatibility.rb +++ b/activesupport/lib/active_support/core_ext/date_time/compatibility.rb @@ -10,7 +10,7 @@ class DateTime # Either return an instance of +Time+ with the same UTC offset # as +self+ or an instance of +Time+ representing the same time - # in the the local system timezone depending on the setting of + # in the local system timezone depending on the setting of # on the setting of +ActiveSupport.to_time_preserves_timezone+. def to_time preserve_timezone ? getlocal(utc_offset) : getlocal diff --git a/activesupport/lib/active_support/core_ext/enumerable.rb b/activesupport/lib/active_support/core_ext/enumerable.rb index 17733d955c..d87d63f287 100644 --- a/activesupport/lib/active_support/core_ext/enumerable.rb +++ b/activesupport/lib/active_support/core_ext/enumerable.rb @@ -1,59 +1,54 @@ # frozen_string_literal: true module Enumerable + INDEX_WITH_DEFAULT = Object.new + private_constant :INDEX_WITH_DEFAULT + # Enumerable#sum was added in Ruby 2.4, but it only works with Numeric elements # when we omit an identity. + + # :stopdoc: + + # 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 + + # :startdoc: + + # Calculates a sum from the elements. # - # 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 + # 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 - # Convert an enumerable to a hash. + # Convert an enumerable to a hash keying it by the block return value. # # people.index_by(&:login) # # => { "nextangle" => <Person ...>, "chade-" => <Person ...>, ...} + # # people.index_by { |person| "#{person.first_name} #{person.last_name}" } # # => { "Chade- Fowlersburg-e" => <Person ...>, "David Heinemeier Hansson" => <Person ...>, ...} def index_by @@ -66,6 +61,26 @@ module Enumerable end end + # Convert an enumerable to a hash keying it with the enumerable items and with the values returned in the block. + # + # post = Post.new(title: "hey there", body: "what's up?") + # + # %i( title body ).index_with { |attr_name| post.public_send(attr_name) } + # # => { title: "hey there", body: "what's up?" } + def index_with(default = INDEX_WITH_DEFAULT) + if block_given? + result = {} + each { |elem| result[elem] = yield(elem) } + result + elsif default != INDEX_WITH_DEFAULT + result = {} + each { |elem| result[elem] = default } + result + else + to_enum(:index_with) { size if respond_to?(:size) } + end + end + # Returns +true+ if the enumerable has more than 1 element. Functionally # equivalent to <tt>enum.to_a.size > 1</tt>. Can be called with a block too, # much like any?, so <tt>people.many? { |p| p.age > 26 }</tt> returns +true+ @@ -133,27 +148,21 @@ class Range #:nodoc: end end -# Array#sum was added in Ruby 2.4 but it only works with Numeric elements. -# -# 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 Array.instance_methods(false).include?(:sum) && !(%w[a].sum rescue false) - # Using Refinements here in order not to expose our internal method - using Module.new { - refine Array do - alias :orig_sum :sum - end - } +# Using Refinements here in order not to expose our internal method +using Module.new { + refine Array do + alias :orig_sum :sum + end +} - class Array - def sum(init = nil, &block) #:nodoc: - if init.is_a?(Numeric) || first.is_a?(Numeric) - init ||= 0 - orig_sum(init, &block) - else - super - end +class Array #:nodoc: + # Array#sum was added in Ruby 2.4 but it only works with Numeric elements. + def sum(init = nil, &block) + if init.is_a?(Numeric) || first.is_a?(Numeric) + init ||= 0 + orig_sum(init, &block) + else + super end end end diff --git a/activesupport/lib/active_support/core_ext/file/atomic.rb b/activesupport/lib/active_support/core_ext/file/atomic.rb index 8e288833b6..9deceb1bb4 100644 --- a/activesupport/lib/active_support/core_ext/file/atomic.rb +++ b/activesupport/lib/active_support/core_ext/file/atomic.rb @@ -29,7 +29,7 @@ class File old_stat = if exist?(file_name) # Get original file permissions stat(file_name) - elsif temp_dir != dirname(file_name) + else # If not possible, probe which are the default permissions in the # destination directory. probe_stat_in(dirname(file_name)) diff --git a/activesupport/lib/active_support/core_ext/hash.rb b/activesupport/lib/active_support/core_ext/hash.rb index e19aeaa983..2f0901d853 100644 --- a/activesupport/lib/active_support/core_ext/hash.rb +++ b/activesupport/lib/active_support/core_ext/hash.rb @@ -1,11 +1,10 @@ # frozen_string_literal: true -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/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" require "active_support/core_ext/hash/reverse_merge" require "active_support/core_ext/hash/slice" -require "active_support/core_ext/hash/transform_values" diff --git a/activesupport/lib/active_support/core_ext/hash/compact.rb b/activesupport/lib/active_support/core_ext/hash/compact.rb index d6364dd9f3..5cb858af5c 100644 --- a/activesupport/lib/active_support/core_ext/hash/compact.rb +++ b/activesupport/lib/active_support/core_ext/hash/compact.rb @@ -1,29 +1,5 @@ # frozen_string_literal: true -class Hash - unless Hash.instance_methods(false).include?(:compact) - # Returns a hash with non +nil+ values. - # - # hash = { a: true, b: false, c: nil } - # hash.compact # => { a: true, b: false } - # hash # => { a: true, b: false, c: nil } - # { c: nil }.compact # => {} - # { c: true }.compact # => { c: true } - def compact - select { |_, value| !value.nil? } - end - end +require "active_support/deprecation" - unless Hash.instance_methods(false).include?(:compact!) - # Replaces current hash with non +nil+ values. - # Returns +nil+ if no changes were made, otherwise returns the hash. - # - # hash = { a: true, b: false, c: nil } - # hash.compact! # => { a: true, b: false } - # hash # => { a: true, b: false } - # { c: true }.compact! # => nil - def compact! - reject! { |_, value| value.nil? } - end - end -end +ActiveSupport::Deprecation.warn "Ruby 2.5+ (required by Rails 6) provides Hash#compact and Hash#compact! natively, so requiring active_support/core_ext/hash/compact is no longer necessary. Requiring it will raise LoadError in Rails 6.1." diff --git a/activesupport/lib/active_support/core_ext/hash/conversions.rb b/activesupport/lib/active_support/core_ext/hash/conversions.rb index 11d28d12a1..5b48254646 100644 --- a/activesupport/lib/active_support/core_ext/hash/conversions.rb +++ b/activesupport/lib/active_support/core_ext/hash/conversions.rb @@ -165,7 +165,7 @@ module ActiveSupport Hash[params.map { |k, v| [k.to_s.tr("-", "_"), normalize_keys(v)] } ] when Array params.map { |v| normalize_keys(v) } - else + else params end end @@ -178,7 +178,7 @@ module ActiveSupport process_array(value) when String value - else + else raise "can't typecast #{value.class.name} - #{value.inspect}" end end 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/keys.rb b/activesupport/lib/active_support/core_ext/hash/keys.rb index bdf196ec3d..7d3495db2c 100644 --- a/activesupport/lib/active_support/core_ext/hash/keys.rb +++ b/activesupport/lib/active_support/core_ext/hash/keys.rb @@ -1,35 +1,6 @@ # frozen_string_literal: true class Hash - # Returns a new hash with all keys converted using the +block+ operation. - # - # hash = { name: 'Rob', age: '28' } - # - # hash.transform_keys { |key| key.to_s.upcase } # => {"NAME"=>"Rob", "AGE"=>"28"} - # - # If you do not provide a +block+, it will return an Enumerator - # for chaining with other methods: - # - # hash.transform_keys.with_index { |k, i| [k, i].join } # => {"name0"=>"Rob", "age1"=>"28"} - def transform_keys - return enum_for(:transform_keys) { size } unless block_given? - result = {} - each_key do |key| - result[yield(key)] = self[key] - end - result - end unless method_defined? :transform_keys - - # Destructively converts all keys using the +block+ operations. - # Same as +transform_keys+ but modifies +self+. - def transform_keys! - return enum_for(:transform_keys!) { size } unless block_given? - keys.each do |key| - self[yield(key)] = delete(key) - end - self - end unless method_defined? :transform_keys! - # Returns a new hash with all keys converted to strings. # # hash = { name: 'Rob', age: '28' } diff --git a/activesupport/lib/active_support/core_ext/hash/slice.rb b/activesupport/lib/active_support/core_ext/hash/slice.rb index 2bd0a56ea4..3d0f8a1e62 100644 --- a/activesupport/lib/active_support/core_ext/hash/slice.rb +++ b/activesupport/lib/active_support/core_ext/hash/slice.rb @@ -1,34 +1,12 @@ # frozen_string_literal: true class Hash - # Slices a hash to include only the given keys. Returns a hash containing - # the given keys. - # - # { a: 1, b: 2, c: 3, d: 4 }.slice(:a, :b) - # # => {:a=>1, :b=>2} - # - # This is useful for limiting an options hash to valid keys before - # passing to a method: - # - # def search(criteria = {}) - # criteria.assert_valid_keys(:mass, :velocity, :time) - # end - # - # search(options.slice(:mass, :velocity, :time)) - # - # If you have an array of keys you want to limit to, you should splat them: - # - # valid_keys = [:mass, :velocity, :time] - # search(options.slice(*valid_keys)) - def slice(*keys) - keys.each_with_object(Hash.new) { |k, hash| hash[k] = self[k] if has_key?(k) } - end unless method_defined?(:slice) - # Replaces the hash with only the given keys. # Returns a hash containing the removed key/value pairs. # - # { a: 1, b: 2, c: 3, d: 4 }.slice!(:a, :b) - # # => {:c=>3, :d=>4} + # hash = { a: 1, b: 2, c: 3, d: 4 } + # hash.slice!(:a, :b) # => {:c=>3, :d=>4} + # hash # => {:a=>1, :b=>2} def slice!(*keys) omit = slice(*self.keys - keys) hash = slice(*keys) diff --git a/activesupport/lib/active_support/core_ext/hash/transform_values.rb b/activesupport/lib/active_support/core_ext/hash/transform_values.rb index 4b19c9fc1f..e4aeb0e891 100644 --- a/activesupport/lib/active_support/core_ext/hash/transform_values.rb +++ b/activesupport/lib/active_support/core_ext/hash/transform_values.rb @@ -1,32 +1,5 @@ # frozen_string_literal: true -class Hash - # Returns a new hash with the results of running +block+ once for every value. - # The keys are unchanged. - # - # { a: 1, b: 2, c: 3 }.transform_values { |x| x * 2 } # => { a: 2, b: 4, c: 6 } - # - # If you do not provide a +block+, it will return an Enumerator - # for chaining with other methods: - # - # { a: 1, b: 2 }.transform_values.with_index { |v, i| [v, i].join.to_i } # => { a: 10, b: 21 } - def transform_values - return enum_for(:transform_values) { size } unless block_given? - return {} if empty? - result = self.class.new - each do |key, value| - result[key] = yield(value) - end - result - end unless method_defined? :transform_values +require "active_support/deprecation" - # Destructively converts all values using the +block+ operations. - # Same as +transform_values+ but modifies +self+. - def transform_values! - return enum_for(:transform_values!) { size } unless block_given? - each do |key, value| - self[key] = yield(value) - end - end unless method_defined? :transform_values! - # TODO: Remove this file when supporting only Ruby 2.4+. -end +ActiveSupport::Deprecation.warn "Ruby 2.5+ (required by Rails 6) provides Hash#transform_values natively, so requiring active_support/core_ext/hash/transform_values is no longer necessary. Requiring it will raise LoadError in Rails 6.1." diff --git a/activesupport/lib/active_support/core_ext/integer/multiple.rb b/activesupport/lib/active_support/core_ext/integer/multiple.rb index e7606662d3..bd57a909c5 100644 --- a/activesupport/lib/active_support/core_ext/integer/multiple.rb +++ b/activesupport/lib/active_support/core_ext/integer/multiple.rb @@ -7,6 +7,6 @@ class Integer # 6.multiple_of?(5) # => false # 10.multiple_of?(2) # => true def multiple_of?(number) - number != 0 ? self % number == 0 : zero? + number == 0 ? self == 0 : self % number == 0 end end 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/load_error.rb b/activesupport/lib/active_support/core_ext/load_error.rb index 750f858fcc..b81ed0605e 100644 --- a/activesupport/lib/active_support/core_ext/load_error.rb +++ b/activesupport/lib/active_support/core_ext/load_error.rb @@ -4,6 +4,6 @@ class LoadError # Returns true if the given path name (except perhaps for the ".rb" # extension) is the missing file which caused the exception to be raised. def is_missing?(location) - location.sub(/\.rb$/, "".freeze) == path.sub(/\.rb$/, "".freeze) + location.sub(/\.rb$/, "") == path.to_s.sub(/\.rb$/, "") end end diff --git a/activesupport/lib/active_support/core_ext/module.rb b/activesupport/lib/active_support/core_ext/module.rb index d91e3fba6a..542af98c04 100644 --- a/activesupport/lib/active_support/core_ext/module.rb +++ b/activesupport/lib/active_support/core_ext/module.rb @@ -3,7 +3,6 @@ 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" 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 580baffa2b..5850e0193f 100644 --- a/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb +++ b/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb @@ -1,8 +1,5 @@ # frozen_string_literal: true -require "active_support/core_ext/array/extract_options" -require "active_support/core_ext/regexp" - # Extends the module object with class/module and instance accessors for # class/module attributes, just like the native attr* accessors for instance # attributes. @@ -163,10 +160,10 @@ class Module # parent class. Similarly if parent class changes the value then that would # change the value of subclasses too. # - # class Male < Person + # class Citizen < Person # end # - # Male.new.hair_colors << :blue + # 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>. 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 4b9b6ea9bd..cb996e9e6d 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,8 +1,5 @@ # frozen_string_literal: true -require "active_support/core_ext/array/extract_options" -require "active_support/core_ext/regexp" - # Extends the module object with class/module and instance accessors for # class/module attributes, just like the native attr* accessors for instance # attributes, but does so on a per-thread basis. @@ -36,9 +33,7 @@ class Module # end # # Current.new.user # => NoMethodError - def thread_mattr_reader(*syms) # :nodoc: - options = syms.extract_options! - + def thread_mattr_reader(*syms, instance_reader: true, instance_accessor: true) # :nodoc: syms.each do |sym| raise NameError.new("invalid attribute name: #{sym}") unless /^[_A-Za-z]\w*$/.match?(sym) @@ -50,7 +45,7 @@ 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} self.class.#{sym} @@ -79,8 +74,7 @@ class Module # end # # Current.new.user = "DHH" # => NoMethodError - def thread_mattr_writer(*syms) # :nodoc: - options = syms.extract_options! + def thread_mattr_writer(*syms, instance_writer: true, instance_accessor: true) # :nodoc: syms.each do |sym| raise NameError.new("invalid attribute name: #{sym}") unless /^[_A-Za-z]\w*$/.match?(sym) @@ -92,7 +86,7 @@ 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) self.class.#{sym} = obj @@ -137,14 +131,14 @@ class Module # Or pass <tt>instance_accessor: false</tt>, to opt out both instance methods. # # class Current - # mattr_accessor :user, instance_accessor: false + # thread_mattr_accessor :user, instance_accessor: false # end # # Current.new.user = "DHH" # => NoMethodError # Current.new.user # => NoMethodError - def thread_mattr_accessor(*syms) - thread_mattr_reader(*syms) - thread_mattr_writer(*syms) + def thread_mattr_accessor(*syms, instance_reader: true, instance_writer: true, instance_accessor: true) + thread_mattr_reader(*syms, instance_reader: instance_reader, instance_accessor: instance_accessor) + thread_mattr_writer(*syms, instance_writer: instance_writer, instance_accessor: instance_accessor) end alias :thread_cattr_accessor :thread_mattr_accessor end diff --git a/activesupport/lib/active_support/core_ext/module/concerning.rb b/activesupport/lib/active_support/core_ext/module/concerning.rb index 800bf213cc..7bbbf321ab 100644 --- a/activesupport/lib/active_support/core_ext/module/concerning.rb +++ b/activesupport/lib/active_support/core_ext/module/concerning.rb @@ -30,7 +30,6 @@ class Module # has_many :events # # before_create :track_creation - # after_destroy :track_deletion # # private # def track_creation @@ -52,7 +51,6 @@ class Module # included do # has_many :events # before_create :track_creation - # after_destroy :track_deletion # end # # private @@ -90,7 +88,6 @@ class Module # included do # has_many :events # before_create :track_creation - # after_destroy :track_deletion # end # # private diff --git a/activesupport/lib/active_support/core_ext/module/delegation.rb b/activesupport/lib/active_support/core_ext/module/delegation.rb index a77f903db5..5652f2d1cc 100644 --- a/activesupport/lib/active_support/core_ext/module/delegation.rb +++ b/activesupport/lib/active_support/core_ext/module/delegation.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true require "set" -require "active_support/core_ext/regexp" class Module # Error generated by +delegate+ when a method is called on +nil+ and +allow_nil+ @@ -20,10 +19,11 @@ class Module # public methods as your own. # # ==== Options - # * <tt>:to</tt> - Specifies the target object + # * <tt>:to</tt> - Specifies the target object name as a symbol or string # * <tt>:prefix</tt> - Prefixes the new method with the target name or a custom prefix - # * <tt>:allow_nil</tt> - if set to true, prevents a +Module::DelegationError+ + # * <tt>:allow_nil</tt> - If set to true, prevents a +Module::DelegationError+ # from being raised + # * <tt>:private</tt> - If set to true, changes method visibility to private # # The macro receives one or more method names (specified as symbols or # strings) and the name of the target object via the <tt>:to</tt> option @@ -114,12 +114,26 @@ class Module # invoice.customer_name # => 'John Doe' # invoice.customer_address # => 'Vimmersvej 13' # + # The delegated methods are public by default. + # Pass <tt>private: true</tt> to change that. + # + # class User < ActiveRecord::Base + # has_one :profile + # delegate :first_name, to: :profile + # delegate :date_of_birth, to: :profile, private: true + # + # def age + # Date.today.year - date_of_birth.year + # end + # end + # + # User.new.first_name # => "Tomas" + # User.new.date_of_birth # => NoMethodError: private method `date_of_birth' called for #<User:0x00000008221340> + # User.new.age # => 2 + # # If the target is +nil+ and does not respond to the delegated method a - # +Module::DelegationError+ is raised, as with any other value. Sometimes, - # however, it makes sense to be robust to that situation and that is the - # purpose of the <tt>:allow_nil</tt> option: If the target is not +nil+, or it - # is and responds to the method, everything works as usual. But if it is +nil+ - # and does not respond to the delegated method, +nil+ is returned. + # +Module::DelegationError+ is raised. If you wish to instead return +nil+, + # use the <tt>:allow_nil</tt> option. # # class User < ActiveRecord::Base # has_one :profile @@ -154,7 +168,7 @@ class Module # Foo.new("Bar").name # raises NoMethodError: undefined method `name' # # The target method must be public, otherwise it will raise +NoMethodError+. - def delegate(*methods, to: nil, prefix: nil, allow_nil: nil) + def delegate(*methods, to: nil, prefix: nil, allow_nil: nil, private: nil) unless to raise ArgumentError, "Delegation needs a target. Supply an options hash with a :to key as the last argument (e.g. delegate :hello, to: :greeter)." end @@ -176,7 +190,7 @@ class Module to = to.to_s to = "self.#{to}" if DELEGATION_RESERVED_METHOD_NAMES.include?(to) - methods.map do |method| + method_names = methods.map do |method| # Attribute writer methods only accept one argument. Makes sure []= # methods still accept two arguments. definition = /[^\]]=$/.match?(method) ? "arg" : "*args, &block" @@ -216,6 +230,9 @@ class Module module_eval(method_def, file, line) end + + private(*method_names) if private + method_names end # When building decorators, a common pattern may emerge: @@ -226,7 +243,7 @@ class Module # end # # def person - # @event.detail.person || @event.creator + # detail.person || creator # end # # private @@ -249,7 +266,7 @@ class Module # end # # def person - # @event.detail.person || @event.creator + # detail.person || creator # end # end # diff --git a/activesupport/lib/active_support/core_ext/module/introspection.rb b/activesupport/lib/active_support/core_ext/module/introspection.rb index c5bb598bd1..9b6df40596 100644 --- a/activesupport/lib/active_support/core_ext/module/introspection.rb +++ b/activesupport/lib/active_support/core_ext/module/introspection.rb @@ -5,8 +5,8 @@ require "active_support/inflector" class Module # Returns the name of the module containing this one. # - # M::N.parent_name # => "M" - def parent_name + # M::N.module_parent_name # => "M" + def module_parent_name if defined?(@parent_name) @parent_name else @@ -16,6 +16,14 @@ class Module end end + def parent_name + ActiveSupport::Deprecation.warn(<<-MSG.squish) + `Module#parent_name` has been renamed to `module_parent_name`. + `parent_name` is deprecated and will be removed in Rails 6.1. + MSG + module_parent_name + end + # Returns the module which contains this one according to its name. # # module M @@ -24,15 +32,23 @@ class Module # end # X = M::N # - # M::N.parent # => M - # X.parent # => M + # M::N.module_parent # => M + # X.module_parent # => M # # The parent of top-level and anonymous modules is Object. # - # M.parent # => Object - # Module.new.parent # => Object + # M.module_parent # => Object + # Module.new.module_parent # => Object + def module_parent + module_parent_name ? ActiveSupport::Inflector.constantize(module_parent_name) : Object + end + def parent - parent_name ? ActiveSupport::Inflector.constantize(parent_name) : Object + ActiveSupport::Deprecation.warn(<<-MSG.squish) + `Module#parent` has been renamed to `module_parent`. + `parent` is deprecated and will be removed in Rails 6.1. + MSG + module_parent end # Returns all the parents of this module according to its name, ordered from @@ -44,13 +60,13 @@ class Module # end # X = M::N # - # M.parents # => [Object] - # M::N.parents # => [M, Object] - # X.parents # => [M, Object] - def parents + # M.module_parents # => [Object] + # M::N.module_parents # => [M, Object] + # X.module_parents # => [M, Object] + def module_parents parents = [] - if parent_name - parts = parent_name.split("::") + if module_parent_name + parts = module_parent_name.split("::") until parts.empty? parents << ActiveSupport::Inflector.constantize(parts * "::") parts.pop @@ -59,4 +75,12 @@ class Module parents << Object unless parents.include? Object parents end + + def parents + ActiveSupport::Deprecation.warn(<<-MSG.squish) + `Module#parents` has been renamed to `module_parents`. + `parents` is deprecated and will be removed in Rails 6.1. + MSG + module_parents + end end diff --git a/activesupport/lib/active_support/core_ext/module/reachable.rb b/activesupport/lib/active_support/core_ext/module/reachable.rb index e9cbda5245..2020f5204c 100644 --- a/activesupport/lib/active_support/core_ext/module/reachable.rb +++ b/activesupport/lib/active_support/core_ext/module/reachable.rb @@ -3,9 +3,4 @@ require "active_support/core_ext/module/anonymous" require "active_support/core_ext/string/inflections" -class Module - def reachable? #:nodoc: - !anonymous? && name.safe_constantize.equal?(self) - end - deprecate :reachable? -end +ActiveSupport::Deprecation.warn("reachable is deprecated and will be removed from the framework.") diff --git a/activesupport/lib/active_support/core_ext/module/redefine_method.rb b/activesupport/lib/active_support/core_ext/module/redefine_method.rb index a0a6622ca4..5bd8e6e973 100644 --- a/activesupport/lib/active_support/core_ext/module/redefine_method.rb +++ b/activesupport/lib/active_support/core_ext/module/redefine_method.rb @@ -1,23 +1,14 @@ # frozen_string_literal: true class Module - if RUBY_VERSION >= "2.3" - # Marks the named method as intended to be redefined, if it exists. - # Suppresses the Ruby method redefinition warning. Prefer - # #redefine_method where possible. - def silence_redefinition_of_method(method) - if method_defined?(method) || private_method_defined?(method) - # This suppresses the "method redefined" warning; the self-alias - # looks odd, but means we don't need to generate a unique name - alias_method method, method - end - end - else - def silence_redefinition_of_method(method) - if method_defined?(method) || private_method_defined?(method) - alias_method :__rails_redefine, method - remove_method :__rails_redefine - end + # Marks the named method as intended to be redefined, if it exists. + # Suppresses the Ruby method redefinition warning. Prefer + # #redefine_method where possible. + def silence_redefinition_of_method(method) + if method_defined?(method) || private_method_defined?(method) + # This suppresses the "method redefined" warning; the self-alias + # looks odd, but means we don't need to generate a unique name + alias_method method, method end end diff --git a/activesupport/lib/active_support/core_ext/name_error.rb b/activesupport/lib/active_support/core_ext/name_error.rb index d4f1e01140..6d37cd9dfd 100644 --- a/activesupport/lib/active_support/core_ext/name_error.rb +++ b/activesupport/lib/active_support/core_ext/name_error.rb @@ -10,6 +10,11 @@ class NameError # end # # => "HelloWorld" def missing_name + # Since ruby v2.3.0 `did_you_mean` gem is loaded by default. + # It extends NameError#message with spell corrections which are SLOW. + # We should use original_message message instead. + message = respond_to?(:original_message) ? original_message : self.message + if /undefined local variable or method/ !~ message $1 if /((::)?([A-Z]\w*)(::[A-Z]\w*)*)$/ =~ message end diff --git a/activesupport/lib/active_support/core_ext/numeric.rb b/activesupport/lib/active_support/core_ext/numeric.rb index 0b04e359f9..fe778470f1 100644 --- a/activesupport/lib/active_support/core_ext/numeric.rb +++ b/activesupport/lib/active_support/core_ext/numeric.rb @@ -2,5 +2,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" diff --git a/activesupport/lib/active_support/core_ext/numeric/conversions.rb b/activesupport/lib/active_support/core_ext/numeric/conversions.rb index f6c2713986..8acad6164a 100644 --- a/activesupport/lib/active_support/core_ext/numeric/conversions.rb +++ b/activesupport/lib/active_support/core_ext/numeric/conversions.rb @@ -4,137 +4,133 @@ require "active_support/core_ext/big_decimal/conversions" require "active_support/number_helper" require "active_support/core_ext/module/deprecation" -module ActiveSupport::NumericWithFormat - # Provides options for converting numbers into formatted strings. - # Options are provided for phone numbers, currency, percentage, - # precision, positional notation, file size and pretty printing. - # - # ==== Options - # - # For details on which formats use which options, see ActiveSupport::NumberHelper - # - # ==== Examples - # - # Phone Numbers: - # 5551234.to_s(:phone) # => "555-1234" - # 1235551234.to_s(:phone) # => "123-555-1234" - # 1235551234.to_s(:phone, area_code: true) # => "(123) 555-1234" - # 1235551234.to_s(:phone, delimiter: ' ') # => "123 555 1234" - # 1235551234.to_s(:phone, area_code: true, extension: 555) # => "(123) 555-1234 x 555" - # 1235551234.to_s(:phone, country_code: 1) # => "+1-123-555-1234" - # 1235551234.to_s(:phone, country_code: 1, extension: 1343, delimiter: '.') - # # => "+1.123.555.1234 x 1343" - # - # Currency: - # 1234567890.50.to_s(:currency) # => "$1,234,567,890.50" - # 1234567890.506.to_s(:currency) # => "$1,234,567,890.51" - # 1234567890.506.to_s(:currency, precision: 3) # => "$1,234,567,890.506" - # 1234567890.506.to_s(:currency, locale: :fr) # => "1 234 567 890,51 €" - # -1234567890.50.to_s(:currency, negative_format: '(%u%n)') - # # => "($1,234,567,890.50)" - # 1234567890.50.to_s(:currency, unit: '£', separator: ',', delimiter: '') - # # => "£1234567890,50" - # 1234567890.50.to_s(:currency, unit: '£', separator: ',', delimiter: '', format: '%n %u') - # # => "1234567890,50 £" - # - # Percentage: - # 100.to_s(:percentage) # => "100.000%" - # 100.to_s(:percentage, precision: 0) # => "100%" - # 1000.to_s(:percentage, delimiter: '.', separator: ',') # => "1.000,000%" - # 302.24398923423.to_s(:percentage, precision: 5) # => "302.24399%" - # 1000.to_s(:percentage, locale: :fr) # => "1 000,000%" - # 100.to_s(:percentage, format: '%n %') # => "100.000 %" - # - # Delimited: - # 12345678.to_s(:delimited) # => "12,345,678" - # 12345678.05.to_s(:delimited) # => "12,345,678.05" - # 12345678.to_s(:delimited, delimiter: '.') # => "12.345.678" - # 12345678.to_s(:delimited, delimiter: ',') # => "12,345,678" - # 12345678.05.to_s(:delimited, separator: ' ') # => "12,345,678 05" - # 12345678.05.to_s(:delimited, locale: :fr) # => "12 345 678,05" - # 98765432.98.to_s(:delimited, delimiter: ' ', separator: ',') - # # => "98 765 432,98" - # - # Rounded: - # 111.2345.to_s(:rounded) # => "111.235" - # 111.2345.to_s(:rounded, precision: 2) # => "111.23" - # 13.to_s(:rounded, precision: 5) # => "13.00000" - # 389.32314.to_s(:rounded, precision: 0) # => "389" - # 111.2345.to_s(:rounded, significant: true) # => "111" - # 111.2345.to_s(:rounded, precision: 1, significant: true) # => "100" - # 13.to_s(:rounded, precision: 5, significant: true) # => "13.000" - # 111.234.to_s(:rounded, locale: :fr) # => "111,234" - # 13.to_s(:rounded, precision: 5, significant: true, strip_insignificant_zeros: true) - # # => "13" - # 389.32314.to_s(:rounded, precision: 4, significant: true) # => "389.3" - # 1111.2345.to_s(:rounded, precision: 2, separator: ',', delimiter: '.') - # # => "1.111,23" - # - # Human-friendly size in Bytes: - # 123.to_s(:human_size) # => "123 Bytes" - # 1234.to_s(:human_size) # => "1.21 KB" - # 12345.to_s(:human_size) # => "12.1 KB" - # 1234567.to_s(:human_size) # => "1.18 MB" - # 1234567890.to_s(:human_size) # => "1.15 GB" - # 1234567890123.to_s(:human_size) # => "1.12 TB" - # 1234567890123456.to_s(:human_size) # => "1.1 PB" - # 1234567890123456789.to_s(:human_size) # => "1.07 EB" - # 1234567.to_s(:human_size, precision: 2) # => "1.2 MB" - # 483989.to_s(:human_size, precision: 2) # => "470 KB" - # 1234567.to_s(:human_size, precision: 2, separator: ',') # => "1,2 MB" - # 1234567890123.to_s(:human_size, precision: 5) # => "1.1228 TB" - # 524288000.to_s(:human_size, precision: 5) # => "500 MB" - # - # Human-friendly format: - # 123.to_s(:human) # => "123" - # 1234.to_s(:human) # => "1.23 Thousand" - # 12345.to_s(:human) # => "12.3 Thousand" - # 1234567.to_s(:human) # => "1.23 Million" - # 1234567890.to_s(:human) # => "1.23 Billion" - # 1234567890123.to_s(:human) # => "1.23 Trillion" - # 1234567890123456.to_s(:human) # => "1.23 Quadrillion" - # 1234567890123456789.to_s(:human) # => "1230 Quadrillion" - # 489939.to_s(:human, precision: 2) # => "490 Thousand" - # 489939.to_s(:human, precision: 4) # => "489.9 Thousand" - # 1234567.to_s(:human, precision: 4, - # significant: false) # => "1.2346 Million" - # 1234567.to_s(:human, precision: 1, - # separator: ',', - # significant: false) # => "1,2 Million" - def to_s(format = nil, options = nil) - case format - when nil - super() - when Integer, String - super(format) - when :phone - ActiveSupport::NumberHelper.number_to_phone(self, options || {}) - when :currency - ActiveSupport::NumberHelper.number_to_currency(self, options || {}) - when :percentage - ActiveSupport::NumberHelper.number_to_percentage(self, options || {}) - when :delimited - ActiveSupport::NumberHelper.number_to_delimited(self, options || {}) - when :rounded - ActiveSupport::NumberHelper.number_to_rounded(self, options || {}) - when :human - ActiveSupport::NumberHelper.number_to_human(self, options || {}) - when :human_size - ActiveSupport::NumberHelper.number_to_human_size(self, options || {}) - when Symbol - super() - else - super(format) +module ActiveSupport + module NumericWithFormat + # Provides options for converting numbers into formatted strings. + # Options are provided for phone numbers, currency, percentage, + # precision, positional notation, file size and pretty printing. + # + # ==== Options + # + # For details on which formats use which options, see ActiveSupport::NumberHelper + # + # ==== Examples + # + # Phone Numbers: + # 5551234.to_s(:phone) # => "555-1234" + # 1235551234.to_s(:phone) # => "123-555-1234" + # 1235551234.to_s(:phone, area_code: true) # => "(123) 555-1234" + # 1235551234.to_s(:phone, delimiter: ' ') # => "123 555 1234" + # 1235551234.to_s(:phone, area_code: true, extension: 555) # => "(123) 555-1234 x 555" + # 1235551234.to_s(:phone, country_code: 1) # => "+1-123-555-1234" + # 1235551234.to_s(:phone, country_code: 1, extension: 1343, delimiter: '.') + # # => "+1.123.555.1234 x 1343" + # + # Currency: + # 1234567890.50.to_s(:currency) # => "$1,234,567,890.50" + # 1234567890.506.to_s(:currency) # => "$1,234,567,890.51" + # 1234567890.506.to_s(:currency, precision: 3) # => "$1,234,567,890.506" + # 1234567890.506.to_s(:currency, locale: :fr) # => "1 234 567 890,51 €" + # -1234567890.50.to_s(:currency, negative_format: '(%u%n)') + # # => "($1,234,567,890.50)" + # 1234567890.50.to_s(:currency, unit: '£', separator: ',', delimiter: '') + # # => "£1234567890,50" + # 1234567890.50.to_s(:currency, unit: '£', separator: ',', delimiter: '', format: '%n %u') + # # => "1234567890,50 £" + # + # Percentage: + # 100.to_s(:percentage) # => "100.000%" + # 100.to_s(:percentage, precision: 0) # => "100%" + # 1000.to_s(:percentage, delimiter: '.', separator: ',') # => "1.000,000%" + # 302.24398923423.to_s(:percentage, precision: 5) # => "302.24399%" + # 1000.to_s(:percentage, locale: :fr) # => "1 000,000%" + # 100.to_s(:percentage, format: '%n %') # => "100.000 %" + # + # Delimited: + # 12345678.to_s(:delimited) # => "12,345,678" + # 12345678.05.to_s(:delimited) # => "12,345,678.05" + # 12345678.to_s(:delimited, delimiter: '.') # => "12.345.678" + # 12345678.to_s(:delimited, delimiter: ',') # => "12,345,678" + # 12345678.05.to_s(:delimited, separator: ' ') # => "12,345,678 05" + # 12345678.05.to_s(:delimited, locale: :fr) # => "12 345 678,05" + # 98765432.98.to_s(:delimited, delimiter: ' ', separator: ',') + # # => "98 765 432,98" + # + # Rounded: + # 111.2345.to_s(:rounded) # => "111.235" + # 111.2345.to_s(:rounded, precision: 2) # => "111.23" + # 13.to_s(:rounded, precision: 5) # => "13.00000" + # 389.32314.to_s(:rounded, precision: 0) # => "389" + # 111.2345.to_s(:rounded, significant: true) # => "111" + # 111.2345.to_s(:rounded, precision: 1, significant: true) # => "100" + # 13.to_s(:rounded, precision: 5, significant: true) # => "13.000" + # 111.234.to_s(:rounded, locale: :fr) # => "111,234" + # 13.to_s(:rounded, precision: 5, significant: true, strip_insignificant_zeros: true) + # # => "13" + # 389.32314.to_s(:rounded, precision: 4, significant: true) # => "389.3" + # 1111.2345.to_s(:rounded, precision: 2, separator: ',', delimiter: '.') + # # => "1.111,23" + # + # Human-friendly size in Bytes: + # 123.to_s(:human_size) # => "123 Bytes" + # 1234.to_s(:human_size) # => "1.21 KB" + # 12345.to_s(:human_size) # => "12.1 KB" + # 1234567.to_s(:human_size) # => "1.18 MB" + # 1234567890.to_s(:human_size) # => "1.15 GB" + # 1234567890123.to_s(:human_size) # => "1.12 TB" + # 1234567890123456.to_s(:human_size) # => "1.1 PB" + # 1234567890123456789.to_s(:human_size) # => "1.07 EB" + # 1234567.to_s(:human_size, precision: 2) # => "1.2 MB" + # 483989.to_s(:human_size, precision: 2) # => "470 KB" + # 1234567.to_s(:human_size, precision: 2, separator: ',') # => "1,2 MB" + # 1234567890123.to_s(:human_size, precision: 5) # => "1.1228 TB" + # 524288000.to_s(:human_size, precision: 5) # => "500 MB" + # + # Human-friendly format: + # 123.to_s(:human) # => "123" + # 1234.to_s(:human) # => "1.23 Thousand" + # 12345.to_s(:human) # => "12.3 Thousand" + # 1234567.to_s(:human) # => "1.23 Million" + # 1234567890.to_s(:human) # => "1.23 Billion" + # 1234567890123.to_s(:human) # => "1.23 Trillion" + # 1234567890123456.to_s(:human) # => "1.23 Quadrillion" + # 1234567890123456789.to_s(:human) # => "1230 Quadrillion" + # 489939.to_s(:human, precision: 2) # => "490 Thousand" + # 489939.to_s(:human, precision: 4) # => "489.9 Thousand" + # 1234567.to_s(:human, precision: 4, + # significant: false) # => "1.2346 Million" + # 1234567.to_s(:human, precision: 1, + # separator: ',', + # significant: false) # => "1,2 Million" + def to_s(format = nil, options = nil) + case format + when nil + super() + when Integer, String + super(format) + when :phone + ActiveSupport::NumberHelper.number_to_phone(self, options || {}) + when :currency + ActiveSupport::NumberHelper.number_to_currency(self, options || {}) + when :percentage + ActiveSupport::NumberHelper.number_to_percentage(self, options || {}) + when :delimited + ActiveSupport::NumberHelper.number_to_delimited(self, options || {}) + when :rounded + ActiveSupport::NumberHelper.number_to_rounded(self, options || {}) + when :human + ActiveSupport::NumberHelper.number_to_human(self, options || {}) + when :human_size + ActiveSupport::NumberHelper.number_to_human_size(self, options || {}) + when Symbol + super() + else + super(format) + end end end end -# Ruby 2.4+ unifies Fixnum & Bignum into Integer. -if 0.class == Integer - Integer.prepend ActiveSupport::NumericWithFormat -else - Fixnum.prepend ActiveSupport::NumericWithFormat - Bignum.prepend ActiveSupport::NumericWithFormat -end +Integer.prepend ActiveSupport::NumericWithFormat Float.prepend ActiveSupport::NumericWithFormat BigDecimal.prepend ActiveSupport::NumericWithFormat diff --git a/activesupport/lib/active_support/core_ext/numeric/inquiry.rb b/activesupport/lib/active_support/core_ext/numeric/inquiry.rb index 15334c91f1..6b5240d051 100644 --- a/activesupport/lib/active_support/core_ext/numeric/inquiry.rb +++ b/activesupport/lib/active_support/core_ext/numeric/inquiry.rb @@ -1,28 +1,5 @@ # frozen_string_literal: true -unless 1.respond_to?(:positive?) # TODO: Remove this file when we drop support to ruby < 2.3 - class Numeric - # Returns true if the number is positive. - # - # 1.positive? # => true - # 0.positive? # => false - # -1.positive? # => false - def positive? - self > 0 - end +require "active_support/deprecation" - # Returns true if the number is negative. - # - # -1.negative? # => true - # 0.negative? # => false - # 1.negative? # => false - def negative? - self < 0 - end - end - - class Complex - undef :positive? - undef :negative? - end -end +ActiveSupport::Deprecation.warn "Ruby 2.5+ (required by Rails 6) provides Numeric#positive? and Numeric#negative? natively, so requiring active_support/core_ext/numeric/inquiry is no longer necessary. Requiring it will raise LoadError in Rails 6.1." diff --git a/activesupport/lib/active_support/core_ext/object/blank.rb b/activesupport/lib/active_support/core_ext/object/blank.rb index e42ad852dd..f36fef6cc9 100644 --- a/activesupport/lib/active_support/core_ext/object/blank.rb +++ b/activesupport/lib/active_support/core_ext/object/blank.rb @@ -1,10 +1,10 @@ # frozen_string_literal: true -require "active_support/core_ext/regexp" +require "concurrent/map" class Object # An object is blank if it's false, empty, or a whitespace string. - # For example, +false+, '', ' ', +nil+, [], and {} are all blank. + # For example, +nil+, '', ' ', [], {}, and +false+ are all blank. # # This simplifies # @@ -102,6 +102,9 @@ end class String BLANK_RE = /\A[[:space:]]*\z/ + ENCODED_BLANKS = Concurrent::Map.new do |h, enc| + h[enc] = Regexp.new(BLANK_RE.source.encode(enc), BLANK_RE.options | Regexp::FIXEDENCODING) + end # A string is blank if it's empty or contains whitespaces only: # @@ -119,7 +122,12 @@ class String # The regexp that matches blank strings is expensive. For the case of empty # strings we can speed up this method (~3.5x) with an empty? call. The # penalty for the rest of strings is marginal. - empty? || BLANK_RE.match?(self) + empty? || + begin + BLANK_RE.match?(self) + rescue Encoding::CompatibilityError + ENCODED_BLANKS[self.encoding].match?(self) + end end end diff --git a/activesupport/lib/active_support/core_ext/object/duplicable.rb b/activesupport/lib/active_support/core_ext/object/duplicable.rb index 9bb99087bc..c78ee6bbfc 100644 --- a/activesupport/lib/active_support/core_ext/object/duplicable.rb +++ b/activesupport/lib/active_support/core_ext/object/duplicable.rb @@ -75,8 +75,11 @@ end class Symbol begin - :symbol.dup # Ruby 2.4.x. - "symbol_from_string".to_sym.dup # Some symbols can't `dup` in Ruby 2.4.0. + :symbol.dup + + # Some symbols couldn't be duped in Ruby 2.4.0 only, due to a bug. + # This feature check catches any regression. + "symbol_from_string".to_sym.dup rescue TypeError # Symbols are not duplicable: diff --git a/activesupport/lib/active_support/core_ext/object/json.rb b/activesupport/lib/active_support/core_ext/object/json.rb index f7c623fe13..416059d17b 100644 --- a/activesupport/lib/active_support/core_ext/object/json.rb +++ b/activesupport/lib/active_support/core_ext/object/json.rb @@ -14,6 +14,7 @@ require "active_support/core_ext/time/conversions" require "active_support/core_ext/date_time/conversions" require "active_support/core_ext/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, # otherwise they will always use to_json gem implementation, which is backwards incompatible in diff --git a/activesupport/lib/active_support/core_ext/object/to_query.rb b/activesupport/lib/active_support/core_ext/object/to_query.rb index abb461966a..bac6ff9c97 100644 --- a/activesupport/lib/active_support/core_ext/object/to_query.rb +++ b/activesupport/lib/active_support/core_ext/object/to_query.rb @@ -75,11 +75,14 @@ class Hash # # This method is also aliased as +to_param+. def to_query(namespace = nil) - collect do |key, value| + query = collect do |key, value| unless (value.is_a?(Hash) || value.is_a?(Array)) && value.empty? value.to_query(namespace ? "#{namespace}[#{key}]" : key) end - end.compact.sort! * "&" + end.compact + + query.sort! unless namespace.to_s.include?("[]") + query.join("&") end alias_method :to_param, :to_query diff --git a/activesupport/lib/active_support/core_ext/object/try.rb b/activesupport/lib/active_support/core_ext/object/try.rb index c874691629..ef8a1f476d 100644 --- a/activesupport/lib/active_support/core_ext/object/try.rb +++ b/activesupport/lib/active_support/core_ext/object/try.rb @@ -4,19 +4,27 @@ require "delegate" module ActiveSupport module Tryable #:nodoc: - def try(*a, &b) - try!(*a, &b) if a.empty? || respond_to?(a.first) + def try(method_name = nil, *args, &b) + if method_name.nil? && block_given? + if b.arity == 0 + instance_eval(&b) + else + yield self + end + elsif respond_to?(method_name) + public_send(method_name, *args, &b) + end end - def try!(*a, &b) - if a.empty? && block_given? + def try!(method_name = nil, *args, &b) + if method_name.nil? && block_given? if b.arity == 0 instance_eval(&b) else yield self end else - public_send(*a, &b) + public_send(method_name, *args, &b) end end end @@ -135,14 +143,14 @@ class NilClass # # With +try+ # @person.try(:children).try(:first).try(:name) - def try(*args) + def try(method_name = nil, *args) nil end # Calling +try!+ on +nil+ always returns +nil+. # # nil.try!(:name) # => nil - def try!(*args) + def try!(method_name = nil, *args) nil end end 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 2838fd76be..1d46add6e0 100644 --- a/activesupport/lib/active_support/core_ext/object/with_options.rb +++ b/activesupport/lib/active_support/core_ext/object/with_options.rb @@ -68,7 +68,7 @@ class Object # You can access these methods using the class name instead: # # class Phone < ActiveRecord::Base - # enum phone_number_type: [home: 0, office: 1, mobile: 2] + # enum phone_number_type: { home: 0, office: 1, mobile: 2 } # # with_options presence: true do # validates :phone_number_type, inclusion: { in: Phone.phone_number_types.keys } diff --git a/activesupport/lib/active_support/core_ext/range.rb b/activesupport/lib/active_support/core_ext/range.rb index 4074e91d17..78814fd189 100644 --- a/activesupport/lib/active_support/core_ext/range.rb +++ b/activesupport/lib/active_support/core_ext/range.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true require "active_support/core_ext/range/conversions" -require "active_support/core_ext/range/include_range" +require "active_support/core_ext/range/compare_range" require "active_support/core_ext/range/include_time_with_zone" require "active_support/core_ext/range/overlaps" require "active_support/core_ext/range/each" diff --git a/activesupport/lib/active_support/core_ext/range/compare_range.rb b/activesupport/lib/active_support/core_ext/range/compare_range.rb new file mode 100644 index 0000000000..6f6d2a27bb --- /dev/null +++ b/activesupport/lib/active_support/core_ext/range/compare_range.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +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 + # + # The native Range#=== behavior is untouched. + # ('a'..'f') === ('c') # => true + # (5..9) === (11) # => false + def ===(value) + if value.is_a?(::Range) + # 1...10 includes 1..9 but it does not include 1..10. + operator = exclude_end? && !value.exclude_end? ? :< : :<= + super(value.first) && value.last.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 + # + # The native Range#include? behavior is untouched. + # ('a'..'f').include?('c') # => true + # (5..9).include?(11) # => false + def include?(value) + if value.is_a?(::Range) + # 1...10 includes 1..9 but it does not include 1..10. + operator = exclude_end? && !value.exclude_end? ? :< : :<= + super(value.first) && value.last.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 + # + # The native Range#cover? behavior is untouched. + # ('a'..'f').cover?('c') # => true + # (5..9).cover?(11) # => false + def cover?(value) + if value.is_a?(::Range) + # 1...10 covers 1..9 but it does not cover 1..10. + operator = exclude_end? && !value.exclude_end? ? :< : :<= + super(value.first) && value.last.send(operator, last) + else + super + end + end + end +end + +Range.prepend(ActiveSupport::CompareWithRange) diff --git a/activesupport/lib/active_support/core_ext/range/conversions.rb b/activesupport/lib/active_support/core_ext/range/conversions.rb index 8832fbcb3c..024e32db40 100644 --- a/activesupport/lib/active_support/core_ext/range/conversions.rb +++ b/activesupport/lib/active_support/core_ext/range/conversions.rb @@ -1,39 +1,41 @@ # frozen_string_literal: true -module ActiveSupport::RangeWithFormat - RANGE_FORMATS = { - db: -> (start, stop) do - case start - when String then "BETWEEN '#{start}' AND '#{stop}'" +module ActiveSupport + module RangeWithFormat + RANGE_FORMATS = { + db: -> (start, stop) do + case start + when String then "BETWEEN '#{start}' AND '#{stop}'" + else + "BETWEEN '#{start.to_s(:db)}' AND '#{stop.to_s(:db)}'" + end + end + } + + # Convert range to a formatted string. See RANGE_FORMATS for predefined formats. + # + # range = (1..100) # => 1..100 + # + # range.to_s # => "1..100" + # range.to_s(:db) # => "BETWEEN '1' AND '100'" + # + # == Adding your own range formats to to_s + # You can add your own formats to the Range::RANGE_FORMATS hash. + # Use the format name as the hash key and a Proc instance. + # + # # config/initializers/range_formats.rb + # Range::RANGE_FORMATS[:short] = ->(start, stop) { "Between #{start.to_s(:db)} and #{stop.to_s(:db)}" } + def to_s(format = :default) + if formatter = RANGE_FORMATS[format] + formatter.call(first, last) else - "BETWEEN '#{start.to_s(:db)}' AND '#{stop.to_s(:db)}'" + super() end end - } - # Convert range to a formatted string. See RANGE_FORMATS for predefined formats. - # - # range = (1..100) # => 1..100 - # - # range.to_s # => "1..100" - # range.to_s(:db) # => "BETWEEN '1' AND '100'" - # - # == Adding your own range formats to to_s - # You can add your own formats to the Range::RANGE_FORMATS hash. - # Use the format name as the hash key and a Proc instance. - # - # # config/initializers/range_formats.rb - # Range::RANGE_FORMATS[:short] = ->(start, stop) { "Between #{start.to_s(:db)} and #{stop.to_s(:db)}" } - def to_s(format = :default) - if formatter = RANGE_FORMATS[format] - formatter.call(first, last) - else - super() - end + alias_method :to_default_s, :to_s + alias_method :to_formatted_s, :to_s end - - alias_method :to_default_s, :to_s - alias_method :to_formatted_s, :to_s end Range.prepend(ActiveSupport::RangeWithFormat) diff --git a/activesupport/lib/active_support/core_ext/range/include_range.rb b/activesupport/lib/active_support/core_ext/range/include_range.rb index 7ba1011921..2da2c587a3 100644 --- a/activesupport/lib/active_support/core_ext/range/include_range.rb +++ b/activesupport/lib/active_support/core_ext/range/include_range.rb @@ -1,25 +1,9 @@ # frozen_string_literal: true -module ActiveSupport - module IncludeWithRange #:nodoc: - # 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 - # - # The native Range#include? behavior is untouched. - # ('a'..'f').include?('c') # => true - # (5..9).include?(11) # => false - def include?(value) - if value.is_a?(::Range) - # 1...10 includes 1..9 but it does not include 1..10. - operator = exclude_end? && !value.exclude_end? ? :< : :<= - super(value.first) && value.last.send(operator, last) - else - super - end - end - end -end +require "active_support/deprecation" -Range.prepend(ActiveSupport::IncludeWithRange) +ActiveSupport::Deprecation.warn "You have required `active_support/core_ext/range/include_range`. " \ +"This file will be removed in Rails 6.1. You should require `active_support/core_ext/range/compare_range` " \ + "instead." + +require "active_support/core_ext/range/compare_range" diff --git a/activesupport/lib/active_support/core_ext/regexp.rb b/activesupport/lib/active_support/core_ext/regexp.rb index efbd708aee..d92943c7ae 100644 --- a/activesupport/lib/active_support/core_ext/regexp.rb +++ b/activesupport/lib/active_support/core_ext/regexp.rb @@ -4,8 +4,4 @@ class Regexp #:nodoc: def multiline? options & MULTILINE == MULTILINE end - - def match?(string, pos = 0) - !!match(string, pos) - end unless //.respond_to?(:match?) end diff --git a/activesupport/lib/active_support/core_ext/securerandom.rb b/activesupport/lib/active_support/core_ext/securerandom.rb index b4a491f5fd..ef812f7e1a 100644 --- a/activesupport/lib/active_support/core_ext/securerandom.rb +++ b/activesupport/lib/active_support/core_ext/securerandom.rb @@ -4,17 +4,18 @@ require "securerandom" module SecureRandom BASE58_ALPHABET = ("0".."9").to_a + ("A".."Z").to_a + ("a".."z").to_a - ["0", "O", "I", "l"] + BASE36_ALPHABET = ("0".."9").to_a + ("a".."z").to_a + # SecureRandom.base58 generates a random base58 string. # - # The argument _n_ specifies the length, of the random string to be generated. + # The argument _n_ specifies the length of the random string to be generated. # # If _n_ is not specified or is +nil+, 16 is assumed. It may be larger in the future. # - # The result may contain alphanumeric characters except 0, O, I and l + # The result may contain alphanumeric characters except 0, O, I and l. # # p SecureRandom.base58 # => "4kUgL2pdQMSCQtjE" # p SecureRandom.base58(24) # => "77TMHrHJFvFDwodq8w7Ev2m7" - # def self.base58(n = 16) SecureRandom.random_bytes(n).unpack("C*").map do |byte| idx = byte % 64 @@ -22,4 +23,23 @@ module SecureRandom BASE58_ALPHABET[idx] end.join end + + # SecureRandom.base36 generates a random base36 string in lowercase. + # + # The argument _n_ specifies the length of the random string to be generated. + # + # If _n_ is not specified or is +nil+, 16 is assumed. It may be larger in the future. + # This method can be used over +base58+ if a deterministic case key is necessary. + # + # The result will contain alphanumeric characters in lowercase. + # + # p SecureRandom.base36 # => "4kugl2pdqmscqtje" + # p SecureRandom.base36(24) # => "77tmhrhjfvfdwodq8w7ev2m7" + def self.base36(n = 16) + SecureRandom.random_bytes(n).unpack("C*").map do |byte| + idx = byte % 64 + idx = SecureRandom.random_number(36) if idx >= 36 + BASE36_ALPHABET[idx] + end.join + end end diff --git a/activesupport/lib/active_support/core_ext/string/access.rb b/activesupport/lib/active_support/core_ext/string/access.rb index 58591bbaaf..4ca24028b0 100644 --- a/activesupport/lib/active_support/core_ext/string/access.rb +++ b/activesupport/lib/active_support/core_ext/string/access.rb @@ -75,6 +75,10 @@ class String # str.first(0) # => "" # str.first(6) # => "hello" def first(limit = 1) + ActiveSupport::Deprecation.warn( + "Calling String#first with a negative integer limit " \ + "will raise an ArgumentError in Rails 6.1." + ) if limit < 0 if limit == 0 "" elsif limit >= size @@ -95,6 +99,10 @@ class String # str.last(0) # => "" # str.last(6) # => "hello" def last(limit = 1) + ActiveSupport::Deprecation.warn( + "Calling String#last with a negative integer limit " \ + "will raise an ArgumentError in Rails 6.1." + ) if limit < 0 if limit == 0 "" elsif limit >= size diff --git a/activesupport/lib/active_support/core_ext/string/filters.rb b/activesupport/lib/active_support/core_ext/string/filters.rb index 66e721eea3..df0e79afa8 100644 --- a/activesupport/lib/active_support/core_ext/string/filters.rb +++ b/activesupport/lib/active_support/core_ext/string/filters.rb @@ -78,6 +78,47 @@ class String "#{self[0, stop]}#{omission}" end + # Truncates +text+ to at most <tt>bytesize</tt> bytes in length without + # breaking string encoding by splitting multibyte characters or breaking + # grapheme clusters ("perceptual characters") by truncating at combining + # characters. + # + # >> "🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪".size + # => 20 + # >> "🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪".bytesize + # => 80 + # >> "🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪".truncate_bytes(20) + # => "🔪🔪🔪🔪…" + # + # The truncated text ends with the <tt>:omission</tt> string, defaulting + # to "…", for a total length not exceeding <tt>bytesize</tt>. + def truncate_bytes(truncate_at, omission: "…") + omission ||= "" + + case + when bytesize <= truncate_at + dup + when omission.bytesize > truncate_at + raise ArgumentError, "Omission #{omission.inspect} is #{omission.bytesize}, larger than the truncation length of #{truncate_at} bytes" + when omission.bytesize == truncate_at + omission.dup + else + self.class.new.tap do |cut| + cut_at = truncate_at - omission.bytesize + + scan(/\X/) do |grapheme| + if cut.bytesize + grapheme.bytesize <= cut_at + cut << grapheme + else + break + end + end + + cut << omission + end + end + end + # Truncates a given +text+ after a given number of words (<tt>words_count</tt>): # # 'Once upon a time in a world far far away'.truncate_words(4) diff --git a/activesupport/lib/active_support/core_ext/string/multibyte.rb b/activesupport/lib/active_support/core_ext/string/multibyte.rb index 07c0d16398..6cceb46507 100644 --- a/activesupport/lib/active_support/core_ext/string/multibyte.rb +++ b/activesupport/lib/active_support/core_ext/string/multibyte.rb @@ -11,12 +11,13 @@ class String # encapsulates the original string. A Unicode safe version of all the String methods are defined on this proxy # class. If the proxy class doesn't respond to a certain method, it's forwarded to the encapsulated string. # - # >> "lj".upcase - # => "lj" # >> "lj".mb_chars.upcase.to_s # => "LJ" # - # NOTE: An above example is useful for pre Ruby 2.4. Ruby 2.4 supports Unicode case mappings. + # NOTE: Ruby 2.4 and later support native Unicode case mappings: + # + # >> "lj".upcase + # => "LJ" # # == Method chaining # 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 f3bdc2977e..3a80de4617 100644 --- a/activesupport/lib/active_support/core_ext/string/output_safety.rb +++ b/activesupport/lib/active_support/core_ext/string/output_safety.rb @@ -134,8 +134,9 @@ end module ActiveSupport #:nodoc: class SafeBuffer < String UNSAFE_STRING_METHODS = %w( - capitalize chomp chop delete downcase gsub lstrip next reverse rstrip - slice squeeze strip sub succ swapcase tr tr_s upcase + 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 ) alias_method :original_concat, :concat @@ -149,9 +150,7 @@ module ActiveSupport #:nodoc: end def [](*args) - if args.size < 2 - super - elsif html_safe? + if html_safe? new_safe_buffer = super if new_safe_buffer @@ -188,10 +187,22 @@ module ActiveSupport #:nodoc: end alias << concat + def insert(index, value) + super(index, html_escape_interpolated_argument(value)) + end + def prepend(value) super(html_escape_interpolated_argument(value)) end + def replace(value) + super(html_escape_interpolated_argument(value)) + end + + def []=(index, value) + super(index, html_escape_interpolated_argument(value)) + end + def +(other) dup.concat(other) end diff --git a/activesupport/lib/active_support/core_ext/string/strip.rb b/activesupport/lib/active_support/core_ext/string/strip.rb index cc26274e4a..60e9952ee6 100644 --- a/activesupport/lib/active_support/core_ext/string/strip.rb +++ b/activesupport/lib/active_support/core_ext/string/strip.rb @@ -20,6 +20,8 @@ class String # Technically, it looks for the least indented non-empty line # in the whole string, and removes that amount of leading whitespace. def strip_heredoc - gsub(/^#{scan(/^[ \t]*(?=\S)/).min}/, "".freeze) + gsub(/^#{scan(/^[ \t]*(?=\S)/).min}/, "").tap do |stripped| + stripped.freeze if frozen? + end end end diff --git a/activesupport/lib/active_support/core_ext/uri.rb b/activesupport/lib/active_support/core_ext/uri.rb index c93c0b5c2d..cdd81ae562 100644 --- a/activesupport/lib/active_support/core_ext/uri.rb +++ b/activesupport/lib/active_support/core_ext/uri.rb @@ -1,10 +1,8 @@ # frozen_string_literal: true require "uri" -str = "\xE6\x97\xA5\xE6\x9C\xAC\xE8\xAA\x9E" # Ni-ho-nn-go in UTF-8, means Japanese. -parser = URI::Parser.new -unless str == parser.unescape(parser.escape(str)) +if RUBY_VERSION < "2.6.0" require "active_support/core_ext/module/redefine_method" URI::Parser.class_eval do silence_redefinition_of_method :unescape @@ -13,7 +11,7 @@ unless str == parser.unescape(parser.escape(str)) # YK: My initial experiments say yes, but let's be sure please enc = str.encoding enc = Encoding::UTF_8 if enc == Encoding::US_ASCII - str.gsub(escaped) { |match| [match[1, 2].hex].pack("C") }.force_encoding(enc) + str.dup.force_encoding(Encoding::ASCII_8BIT).gsub(escaped) { |match| [match[1, 2].hex].pack("C") }.force_encoding(enc) end end end diff --git a/activesupport/lib/active_support/current_attributes.rb b/activesupport/lib/active_support/current_attributes.rb index 4e6d8e4585..67ebe102d7 100644 --- a/activesupport/lib/active_support/current_attributes.rb +++ b/activesupport/lib/active_support/current_attributes.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require "active_support/callbacks" + 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 @@ -117,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 82c10b3079..d5d00b5e6e 100644 --- a/activesupport/lib/active_support/dependencies.rb +++ b/activesupport/lib/active_support/dependencies.rb @@ -79,6 +79,12 @@ module ActiveSupport #:nodoc: # to allow arbitrary constants to be marked for unloading. mattr_accessor :explicitly_unloadable_constants, default: [] + # The logger used when tracing autoloads. + mattr_accessor :logger + + # If true, trace autoloads with +logger.debug+. + mattr_accessor :verbose, default: false + # 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 # load of another file (child.rb) the stack will ensure that child.rb @@ -97,6 +103,8 @@ module ActiveSupport #:nodoc: # parent.rb then requires namespace/child.rb, the stack will look like # [[Object], [Namespace]]. + attr_reader :watching + def initialize @watching = [] @stack = Hash.new { |h, k| h[k] = [] } @@ -138,7 +146,7 @@ module ActiveSupport #:nodoc: # Normalize the list of new constants, and add them to the list we will return new_constants.each do |suffix| - constants << ([namespace, suffix] - ["Object"]).join("::".freeze) + constants << ([namespace, suffix] - ["Object"]).join("::") end end constants @@ -224,6 +232,8 @@ module ActiveSupport #:nodoc: Dependencies.require_or_load(file_name) end + # :doc: + # Interprets a file using <tt>mechanism</tt> and marks its defined # constants as autoloaded. <tt>file_name</tt> can be either a string or # respond to <tt>to_path</tt>. @@ -242,9 +252,13 @@ module ActiveSupport #:nodoc: Dependencies.depend_on(file_name, message) end + # :nodoc: + def load_dependency(file) if Dependencies.load? && Dependencies.constant_watch_stack.watching? - Dependencies.new_constants_in(Object) { yield } + descs = Dependencies.constant_watch_stack.watching.flatten.uniq + + Dependencies.new_constants_in(*descs) { yield } else yield end @@ -341,7 +355,7 @@ module ActiveSupport #:nodoc: end def require_or_load(file_name, const_path = nil) - file_name = $` if file_name =~ /\.rb\z/ + file_name = file_name.chomp(".rb") expanded = File.expand_path(file_name) return if loaded.include?(expanded) @@ -391,7 +405,7 @@ module ActiveSupport #:nodoc: # constant paths which would cause Dependencies to attempt to load this # file. def loadable_constants_for_path(path, bases = autoload_paths) - path = $` if path =~ /\.rb\z/ + path = path.chomp(".rb") expanded_path = File.expand_path(path) paths = [] @@ -400,7 +414,7 @@ module ActiveSupport #:nodoc: next unless expanded_path.start_with?(expanded_root) root_size = expanded_root.size - next if expanded_path[root_size] != ?/.freeze + next if expanded_path[root_size] != ?/ nesting = expanded_path[(root_size + 1)..-1] paths << nesting.camelize unless nesting.blank? @@ -412,7 +426,7 @@ module ActiveSupport #:nodoc: # Search for a file in autoload_paths matching the provided suffix. def search_for_file(path_suffix) - path_suffix = path_suffix.sub(/(\.rb)?$/, ".rb".freeze) + path_suffix += ".rb" unless path_suffix.ends_with?(".rb") autoload_paths.each do |root| path = File.join(root, path_suffix) @@ -446,7 +460,9 @@ module ActiveSupport #:nodoc: return nil unless base_path = autoloadable_module?(path_suffix) mod = Module.new into.const_set const_name, mod + log("constant #{qualified_name} autoloaded (module autovivified from #{File.join(base_path, path_suffix)})") autoloaded_constants << qualified_name unless autoload_once_paths.include?(base_path) + autoloaded_constants.uniq! mod end @@ -486,26 +502,31 @@ module ActiveSupport #:nodoc: raise ArgumentError, "A copy of #{from_mod} has been removed from the module tree but is still active!" end - qualified_name = qualified_name_for from_mod, const_name + qualified_name = qualified_name_for(from_mod, const_name) path_suffix = qualified_name.underscore file_path = search_for_file(path_suffix) if file_path expanded = File.expand_path(file_path) - expanded.sub!(/\.rb\z/, "".freeze) + expanded.sub!(/\.rb\z/, "") if loading.include?(expanded) raise "Circular dependency detected while autoloading constant #{qualified_name}" else require_or_load(expanded, qualified_name) - raise LoadError, "Unable to autoload constant #{qualified_name}, expected #{file_path} to define it" unless from_mod.const_defined?(const_name, false) - return from_mod.const_get(const_name) + + if from_mod.const_defined?(const_name, false) + log("constant #{qualified_name} autoloaded from #{expanded}.rb") + return from_mod.const_get(const_name) + else + raise LoadError, "Unable to autoload constant #{qualified_name}, expected #{file_path} to define it" + end end elsif mod = autoload_module!(from_mod, const_name, qualified_name, path_suffix) return mod - elsif (parent = from_mod.parent) && parent != from_mod && - ! from_mod.parents.any? { |p| p.const_defined?(const_name, false) } + elsif (parent = from_mod.module_parent) && parent != from_mod && + ! from_mod.module_parents.any? { |p| p.const_defined?(const_name, false) } # If our parents do not have a constant named +const_name+ then we are free # to attempt to load upwards. If they do have such a constant, then this # const_missing must be due to from_mod::const_name, which should not @@ -549,6 +570,7 @@ module ActiveSupport #:nodoc: # as the environment will be in an inconsistent state, e.g. other constants # may have already been unloaded and not accessible. def remove_unloadable_constants! + log("removing unloadable constants") autoloaded_constants.each { |const| remove_constant const } autoloaded_constants.clear Reference.clear! @@ -670,7 +692,7 @@ module ActiveSupport #:nodoc: when Module desc.name || raise(ArgumentError, "Anonymous modules have no name to be referenced by") - else raise TypeError, "Not a valid constant descriptor: #{desc.inspect}" + else raise TypeError, "Not a valid constant descriptor: #{desc.inspect}" end end @@ -738,6 +760,10 @@ module ActiveSupport #:nodoc: # The constant is no longer reachable, just skip it. end end + + def log(message) + logger.debug("autoloading: #{message}") if logger && verbose + end end end 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..23c237796e --- /dev/null +++ b/activesupport/lib/active_support/dependencies/zeitwerk_integration.rb @@ -0,0 +1,78 @@ +# frozen_string_literal: true + +module ActiveSupport + module Dependencies + module ZeitwerkIntegration # :nodoc: all + module Decorations + def clear + Dependencies.unload_interlock do + Rails.autoloaders.main.reload + end + end + + def constantize(cpath) + Inflector.constantize(cpath) + end + + def safe_constantize(cpath) + Inflector.safe_constantize(cpath) + end + + def autoloaded_constants + (Rails.autoloaders.main.loaded + Rails.autoloaders.once.loaded).to_a + end + + def autoloaded?(object) + cpath = object.is_a?(Module) ? object.name : object.to_s + Rails.autoloaders.any? { |autoloader| autoloader.loaded?(cpath) } + end + + def verbose=(verbose) + l = verbose ? (logger || Rails.logger).method(:debug) : nil + Rails.autoloaders.each { |autoloader| autoloader.logger = l } + end + end + + class << self + def take_over + setup_autoloaders + freeze_autoload_paths + decorate_dependencies + end + + private + + def setup_autoloaders + Dependencies.autoload_paths.each do |autoload_path| + # Zeitwerk only accepts existing directories in `push_dir` to + # prevent misconfigurations. + next unless File.directory?(autoload_path) + + if autoload_once?(autoload_path) + Rails.autoloaders.once.push_dir(autoload_path) + else + Rails.autoloaders.main.push_dir(autoload_path) + end + end + + Rails.autoloaders.each(&:setup) + end + + def autoload_once?(autoload_path) + Dependencies.autoload_once_paths.include?(autoload_path) || + Gem.path.any? { |gem_path| autoload_path.to_s.start_with?(gem_path) } + end + + def freeze_autoload_paths + Dependencies.autoload_paths.freeze + Dependencies.autoload_once_paths.freeze + end + + def decorate_dependencies + 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/deprecation.rb b/activesupport/lib/active_support/deprecation.rb index a1ad2ca465..7271ab565b 100644 --- a/activesupport/lib/active_support/deprecation.rb +++ b/activesupport/lib/active_support/deprecation.rb @@ -35,7 +35,7 @@ module ActiveSupport # and the second is a library name. # # ActiveSupport::Deprecation.new('2.0', 'MyLibrary') - def initialize(deprecation_horizon = "6.0", gem_name = "Rails") + def initialize(deprecation_horizon = "6.1", 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 581db5f449..725667d139 100644 --- a/activesupport/lib/active_support/deprecation/behaviors.rb +++ b/activesupport/lib/active_support/deprecation/behaviors.rb @@ -43,7 +43,7 @@ module ActiveSupport deprecation_horizon: deprecation_horizon) }, - silence: ->(message, callstack, deprecation_horizon, gem_name) {}, + silence: ->(message, callstack, deprecation_horizon, gem_name) { }, } # Behavior module allows to determine how to display deprecation messages. @@ -85,7 +85,7 @@ module ActiveSupport # ActiveSupport::Deprecation.behavior = :stderr # ActiveSupport::Deprecation.behavior = [:stderr, :log] # ActiveSupport::Deprecation.behavior = MyCustomHandler - # ActiveSupport::Deprecation.behavior = ->(message, callstack) { + # ActiveSupport::Deprecation.behavior = ->(message, callstack, deprecation_horizon, gem_name) { # # custom stuff # } def behavior=(behavior) @@ -94,6 +94,10 @@ module ActiveSupport private def arity_coerce(behavior) + unless behavior.respond_to?(:call) + raise ArgumentError, "#{behavior.inspect} is not a valid deprecation behavior." + end + if behavior.arity == 4 || behavior.arity == -1 behavior else diff --git a/activesupport/lib/active_support/deprecation/method_wrappers.rb b/activesupport/lib/active_support/deprecation/method_wrappers.rb index 5be893d281..d99571790f 100644 --- a/activesupport/lib/active_support/deprecation/method_wrappers.rb +++ b/activesupport/lib/active_support/deprecation/method_wrappers.rb @@ -1,6 +1,5 @@ # frozen_string_literal: true -require "active_support/core_ext/module/aliasing" require "active_support/core_ext/array/extract_options" module ActiveSupport @@ -53,24 +52,37 @@ module ActiveSupport options = method_names.extract_options! deprecator = options.delete(:deprecator) || self method_names += options.keys + mod = Module.new - mod = Module.new do - method_names.each do |method_name| - define_method(method_name) do |*args, &block| + method_names.each do |method_name| + if target_module.method_defined?(method_name) || target_module.private_method_defined?(method_name) + aliased_method, punctuation = method_name.to_s.sub(/([?!=])$/, ""), $1 + with_method = "#{aliased_method}_with_deprecation#{punctuation}" + without_method = "#{aliased_method}_without_deprecation#{punctuation}" + + target_module.define_method(with_method) do |*args, &block| deprecator.deprecation_warning(method_name, options[method_name]) - super(*args, &block) + send(without_method, *args, &block) end + target_module.alias_method(without_method, method_name) + target_module.alias_method(method_name, with_method) + case - when target_module.protected_method_defined?(method_name) - protected method_name - when target_module.private_method_defined?(method_name) - private method_name + when target_module.protected_method_defined?(without_method) + target_module.send(:protected, method_name) + when target_module.private_method_defined?(without_method) + target_module.send(:private, method_name) + end + else + mod.define_method(method_name) do |*args, &block| + deprecator.deprecation_warning(method_name, options[method_name]) + super(*args, &block) end end end - target_module.prepend(mod) + target_module.prepend(mod) unless mod.instance_methods(false).empty? end end end diff --git a/activesupport/lib/active_support/deprecation/proxy_wrappers.rb b/activesupport/lib/active_support/deprecation/proxy_wrappers.rb index 896c0d2d8e..56f1e23136 100644 --- a/activesupport/lib/active_support/deprecation/proxy_wrappers.rb +++ b/activesupport/lib/active_support/deprecation/proxy_wrappers.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require "active_support/core_ext/regexp" - module ActiveSupport class Deprecation class DeprecationProxy #:nodoc: diff --git a/activesupport/lib/active_support/deprecation/reporting.rb b/activesupport/lib/active_support/deprecation/reporting.rb index 242e21b782..7075b5b869 100644 --- a/activesupport/lib/active_support/deprecation/reporting.rb +++ b/activesupport/lib/active_support/deprecation/reporting.rb @@ -61,7 +61,7 @@ module ActiveSupport case message when Symbol then "#{warning} (use #{message} instead)" when String then "#{warning} (#{message})" - else warning + else warning end end @@ -104,7 +104,7 @@ module ActiveSupport end end - RAILS_GEM_ROOT = File.expand_path("../../../..", __dir__) + 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/descendants_tracker.rb b/activesupport/lib/active_support/descendants_tracker.rb index a4cee788b6..05236d3162 100644 --- a/activesupport/lib/active_support/descendants_tracker.rb +++ b/activesupport/lib/active_support/descendants_tracker.rb @@ -38,12 +38,13 @@ module ActiveSupport 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) } + + 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) } + end end - end end def inherited(base) diff --git a/activesupport/lib/active_support/digest.rb b/activesupport/lib/active_support/digest.rb index d77eeb072b..fba10fbdcf 100644 --- a/activesupport/lib/active_support/digest.rb +++ b/activesupport/lib/active_support/digest.rb @@ -13,16 +13,8 @@ module ActiveSupport end def hexdigest(arg) - new.hexdigest(arg) + hash_digest_class.hexdigest(arg)[0...32] end end - - def initialize(digest_class: nil) - @digest_class = digest_class || self.class.hash_digest_class - end - - def hexdigest(arg) - @digest_class.hexdigest(arg).truncate(32) - end end end diff --git a/activesupport/lib/active_support/duration.rb b/activesupport/lib/active_support/duration.rb index 1af3411a8a..97b4634d7b 100644 --- a/activesupport/lib/active_support/duration.rb +++ b/activesupport/lib/active_support/duration.rb @@ -194,7 +194,6 @@ module ActiveSupport end parts[:seconds] = remainder - parts.reject! { |k, v| v.zero? } new(value, parts) end @@ -211,11 +210,15 @@ module ActiveSupport def initialize(value, parts) #:nodoc: @value, @parts = value, parts.to_h @parts.default = 0 + @parts.reject! { |k, v| v.zero? } end def coerce(other) #:nodoc: - if Scalar === other + case other + when Scalar [other, self] + when Duration + [Scalar.new(other.value), self] else [Scalar.new(other), self] end @@ -370,8 +373,9 @@ module ActiveSupport alias :before :ago def inspect #:nodoc: + return "0 seconds" if parts.empty? + parts. - reduce(::Hash.new(0)) { |h, (l, r)| h[l] += r; h }. sort_by { |unit, _ | PARTS.index(unit) }. map { |unit, val| "#{val} #{val == 1 ? unit.to_s.chop : unit.to_s}" }. to_sentence(locale: ::I18n.default_locale) @@ -381,6 +385,14 @@ module ActiveSupport to_i end + def init_with(coder) #:nodoc: + initialize(coder["value"], coder["parts"]) + end + + def encode_with(coder) #:nodoc: + coder.map = { "value" => @value, "parts" => @parts } + 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) diff --git a/activesupport/lib/active_support/duration/iso8601_parser.rb b/activesupport/lib/active_support/duration/iso8601_parser.rb index 1847eeaa86..d3233e6111 100644 --- a/activesupport/lib/active_support/duration/iso8601_parser.rb +++ b/activesupport/lib/active_support/duration/iso8601_parser.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true require "strscan" -require "active_support/core_ext/regexp" module ActiveSupport class Duration @@ -14,8 +13,8 @@ module ActiveSupport class ParsingError < ::ArgumentError; end PERIOD_OR_COMMA = /\.|,/ - PERIOD = ".".freeze - COMMA = ",".freeze + PERIOD = "." + COMMA = "," SIGN_MARKER = /\A\-|\+|/ DATE_MARKER = /P/ diff --git a/activesupport/lib/active_support/duration/iso8601_serializer.rb b/activesupport/lib/active_support/duration/iso8601_serializer.rb index bb177ae5b7..1125454919 100644 --- a/activesupport/lib/active_support/duration/iso8601_serializer.rb +++ b/activesupport/lib/active_support/duration/iso8601_serializer.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true require "active_support/core_ext/object/blank" -require "active_support/core_ext/hash/transform_values" module ActiveSupport class Duration @@ -15,14 +14,14 @@ module ActiveSupport # Builds and returns output string. def serialize parts, sign = normalize - return "PT0S".freeze if parts.empty? + return "PT0S" if parts.empty? - output = "P".dup + output = +"P" 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 = "".dup + time = +"" 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/encrypted_configuration.rb b/activesupport/lib/active_support/encrypted_configuration.rb index c52d3869de..cc1d026737 100644 --- a/activesupport/lib/active_support/encrypted_configuration.rb +++ b/activesupport/lib/active_support/encrypted_configuration.rb @@ -11,8 +11,9 @@ module ActiveSupport delegate :[], :fetch, to: :config delegate_missing_to :options - def initialize(config_path:, key_path:, env_key:) - super content_path: config_path, key_path: key_path, env_key: env_key + def initialize(config_path:, key_path:, env_key:, raise_if_missing_key:) + super content_path: config_path, key_path: key_path, + env_key: env_key, raise_if_missing_key: raise_if_missing_key end # Allow a config to be started without a file present @@ -37,12 +38,8 @@ module ActiveSupport @options ||= ActiveSupport::InheritableOptions.new(config) end - def serialize(config) - config.present? ? YAML.dump(config) : "" - end - def deserialize(config) - config.present? ? YAML.load(config, content_path) : {} + YAML.load(config).presence || {} end end end diff --git a/activesupport/lib/active_support/encrypted_file.rb b/activesupport/lib/active_support/encrypted_file.rb index 3d1455fb95..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 @@ -26,11 +27,11 @@ module ActiveSupport end - attr_reader :content_path, :key_path, :env_key + attr_reader :content_path, :key_path, :env_key, :raise_if_missing_key - def initialize(content_path:, key_path:, env_key:) + def initialize(content_path:, key_path:, env_key:, raise_if_missing_key:) @content_path, @key_path = Pathname.new(content_path), Pathname.new(key_path) - @env_key = env_key + @env_key, @raise_if_missing_key = env_key, raise_if_missing_key end def key @@ -38,7 +39,7 @@ module ActiveSupport end def read - if content_path.exist? + if !key.nil? && content_path.exist? decrypt content_path.binread else raise MissingContentError, content_path @@ -57,7 +58,7 @@ module ActiveSupport private def writing(contents) - tmp_file = "#{content_path.basename}.#{Process.pid}" + tmp_file = "#{Process.pid}.#{content_path.basename.to_s.chomp('.enc')}" tmp_path = Pathname.new File.join(Dir.tmpdir, tmp_file) tmp_path.binwrite contents @@ -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 @@ -93,7 +94,7 @@ module ActiveSupport end def handle_missing_key - raise MissingKeyError, key_path: key_path, env_key: env_key + raise MissingKeyError, key_path: key_path, env_key: env_key if raise_if_missing_key end end end diff --git a/activesupport/lib/active_support/evented_file_update_checker.rb b/activesupport/lib/active_support/evented_file_update_checker.rb index 97e982eb05..54bc5c0ae5 100644 --- a/activesupport/lib/active_support/evented_file_update_checker.rb +++ b/activesupport/lib/active_support/evented_file_update_checker.rb @@ -52,16 +52,17 @@ module ActiveSupport @pid = Process.pid @boot_mutex = Mutex.new - if (@dtw = directories_to_watch).any? + dtw = directories_to_watch + @dtw, @missing = dtw.partition(&:exist?) + + if @dtw.any? # Loading listen triggers warnings. These are originated by a legit # usage of attr_* macros for private attributes, but adds a lot of noise # to our test suite. Thus, we lazy load it and disable warnings locally. silence_warnings do - begin - require "listen" - rescue LoadError => e - raise LoadError, "Could not load the 'listen' gem. Add `gem 'listen'` to the development group of your Gemfile", e.backtrace - end + require "listen" + rescue LoadError => e + raise LoadError, "Could not load the 'listen' gem. Add `gem 'listen'` to the development group of your Gemfile", e.backtrace end end boot! @@ -75,6 +76,19 @@ module ActiveSupport @updated.make_true end end + + if @missing.any?(&:exist?) + @boot_mutex.synchronize do + appeared, @missing = @missing.partition(&:exist?) + shutdown! + + @dtw += appeared + boot! + + @updated.make_true + end + end + @updated.true? end @@ -96,6 +110,10 @@ module ActiveSupport Listen.to(*@dtw, &method(:changed)).start end + def shutdown! + Listen.stop + end + def changed(modified, added, removed) unless updated? @updated.make_true if (modified + added + removed).any? { |f| watching?(f) } @@ -123,7 +141,7 @@ module ActiveSupport end def directories_to_watch - dtw = (@files + @dirs.keys).map { |f| @ph.existing_parent(f) } + dtw = @files.map(&:dirname) + @dirs.keys dtw.compact! dtw.uniq! diff --git a/activesupport/lib/active_support/execution_wrapper.rb b/activesupport/lib/active_support/execution_wrapper.rb index f48c586cad..ca810db584 100644 --- a/activesupport/lib/active_support/execution_wrapper.rb +++ b/activesupport/lib/active_support/execution_wrapper.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require "active_support/callbacks" +require "concurrent/hash" module ActiveSupport class ExecutionWrapper diff --git a/activesupport/lib/active_support/gem_version.rb b/activesupport/lib/active_support/gem_version.rb index 1e09adbb52..8bf016e88c 100644 --- a/activesupport/lib/active_support/gem_version.rb +++ b/activesupport/lib/active_support/gem_version.rb @@ -7,10 +7,10 @@ module ActiveSupport end module VERSION - MAJOR = 5 - MINOR = 2 + MAJOR = 6 + MINOR = 0 TINY = 0 - PRE = "beta2" + PRE = "beta1" 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 2e2ed8a25d..3a2b2652c4 100644 --- a/activesupport/lib/active_support/hash_with_indifferent_access.rb +++ b/activesupport/lib/active_support/hash_with_indifferent_access.rb @@ -2,6 +2,7 @@ require "active_support/core_ext/hash/keys" require "active_support/core_ext/hash/reverse_merge" +require "active_support/core_ext/hash/except" module ActiveSupport # Implements a hash where keys <tt>:foo</tt> and <tt>"foo"</tt> are considered @@ -163,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: # @@ -177,20 +191,18 @@ module ActiveSupport super(convert_key(key), *extras) end - if Hash.new.respond_to?(:dig) - # Same as <tt>Hash#dig</tt> where the key passed as argument can be - # either a string or a symbol: - # - # counters = ActiveSupport::HashWithIndifferentAccess.new - # counters[:foo] = { bar: 1 } - # - # counters.dig('foo', 'bar') # => 1 - # counters.dig(:foo, :bar) # => 1 - # counters.dig(:zoo) # => nil - def dig(*args) - args[0] = convert_key(args[0]) if args.size > 0 - super(*args) - end + # Same as <tt>Hash#dig</tt> where the key passed as argument can be + # either a string or a symbol: + # + # counters = ActiveSupport::HashWithIndifferentAccess.new + # counters[:foo] = { bar: 1 } + # + # counters.dig('foo', 'bar') # => 1 + # counters.dig(:foo, :bar) # => 1 + # counters.dig(:zoo) # => nil + def dig(*args) + args[0] = convert_key(args[0]) if args.size > 0 + super(*args) end # Same as <tt>Hash#default</tt> where the key passed as argument can be @@ -228,7 +240,7 @@ module ActiveSupport # 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) + end # Returns a shallow copy of the hash. # @@ -281,6 +293,8 @@ module ActiveSupport super(convert_key(key)) end + alias_method :without, :except + def stringify_keys!; self end def deep_stringify_keys!; self end def stringify_keys; dup end @@ -288,6 +302,7 @@ module ActiveSupport undef :symbolize_keys! undef :deep_symbolize_keys! def symbolize_keys; to_hash.symbolize_keys! end + alias_method :to_options, :symbolize_keys def deep_symbolize_keys; to_hash.deep_symbolize_keys! end def to_options!; self end @@ -311,6 +326,14 @@ module ActiveSupport dup.tap { |hash| hash.transform_keys!(*args, &block) } end + def transform_keys! + return enum_for(:transform_keys!) { size } unless block_given? + keys.each do |key| + self[yield(key)] = delete(key) + end + self + end + def slice(*keys) keys.map! { |key| convert_key(key) } self.class.new(super) diff --git a/activesupport/lib/active_support/i18n.rb b/activesupport/lib/active_support/i18n.rb index d60b3eff30..39dab1cc71 100644 --- a/activesupport/lib/active_support/i18n.rb +++ b/activesupport/lib/active_support/i18n.rb @@ -13,3 +13,4 @@ require "active_support/lazy_load_hooks" ActiveSupport.run_load_hooks(:i18n) I18n.load_path << File.expand_path("locale/en.yml", __dir__) +I18n.load_path << File.expand_path("locale/en.rb", __dir__) diff --git a/activesupport/lib/active_support/i18n_railtie.rb b/activesupport/lib/active_support/i18n_railtie.rb index ce8bfbfd8c..584930e413 100644 --- a/activesupport/lib/active_support/i18n_railtie.rb +++ b/activesupport/lib/active_support/i18n_railtie.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true require "active_support" -require "active_support/file_update_checker" require "active_support/core_ext/array/wrap" # :enddoc: @@ -88,9 +87,21 @@ module I18n when Hash, Array Array.wrap(fallbacks) else # TrueClass - [] + [I18n.default_locale] end + if args.empty? || args.first.is_a?(Hash) + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Using I18n fallbacks with an empty `defaults` sets the defaults to + include the `default_locale`. This behavior will change in Rails 6.1. + If you desire the default locale to be included in the defaults, please + explicitly configure it with `config.i18n.fallbacks.defaults = + [I18n.default_locale]` or `config.i18n.fallbacks = [I18n.default_locale, + {...}]` + MSG + args.unshift I18n.default_locale + end + I18n.fallbacks = I18n::Locale::Fallbacks.new(*args) end diff --git a/activesupport/lib/active_support/inflector/inflections.rb b/activesupport/lib/active_support/inflector/inflections.rb index 0450a4be4c..88cdd99dbd 100644 --- a/activesupport/lib/active_support/inflector/inflections.rb +++ b/activesupport/lib/active_support/inflector/inflections.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true require "concurrent/map" -require "active_support/core_ext/array/prepend_and_append" -require "active_support/core_ext/regexp" require "active_support/i18n" require "active_support/deprecation" @@ -67,8 +65,7 @@ module ActiveSupport @__instance__[locale] ||= new end - attr_reader :plurals, :singulars, :uncountables, :humans, :acronyms, :acronym_regex - deprecate :acronym_regex + attr_reader :plurals, :singulars, :uncountables, :humans, :acronyms attr_reader :acronyms_camelize_regex, :acronyms_underscore_regex # :nodoc: @@ -227,7 +224,7 @@ module ActiveSupport case scope when :all @plurals, @singulars, @uncountables, @humans = [], [], Uncountables.new, [] - else + else instance_variable_set "@#{scope}", [] end end diff --git a/activesupport/lib/active_support/inflector/methods.rb b/activesupport/lib/active_support/inflector/methods.rb index 60eeaa77cb..ee193add6f 100644 --- a/activesupport/lib/active_support/inflector/methods.rb +++ b/activesupport/lib/active_support/inflector/methods.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true require "active_support/inflections" -require "active_support/core_ext/regexp" module ActiveSupport # The Inflector transforms words from singular to plural, class names to table @@ -74,7 +73,7 @@ module ActiveSupport string = string.sub(inflections.acronyms_camelize_regex) { |match| match.downcase } end string.gsub!(/(?:_|(\/))([a-z\d]*)/i) { "#{$1}#{inflections.acronyms[$2] || $2.capitalize}" } - string.gsub!("/".freeze, "::".freeze) + string.gsub!("/", "::") string end @@ -91,11 +90,11 @@ module ActiveSupport # camelize(underscore('SSLError')) # => "SslError" def underscore(camel_cased_word) return camel_cased_word unless /[A-Z-]|::/.match?(camel_cased_word) - word = camel_cased_word.to_s.gsub("::".freeze, "/".freeze) - word.gsub!(inflections.acronyms_underscore_regex) { "#{$1 && '_'.freeze }#{$2.downcase}" } - word.gsub!(/([A-Z\d]+)([A-Z][a-z])/, '\1_\2'.freeze) - word.gsub!(/([a-z\d])([A-Z])/, '\1_\2'.freeze) - word.tr!("-".freeze, "_".freeze) + word = camel_cased_word.to_s.gsub("::", "/") + word.gsub!(inflections.acronyms_underscore_regex) { "#{$1 && '_' }#{$2.downcase}" } + word.gsub!(/([A-Z\d]+)([A-Z][a-z])/, '\1_\2') + word.gsub!(/([a-z\d])([A-Z])/, '\1_\2') + word.tr!("-", "_") word.downcase! word end @@ -131,11 +130,11 @@ module ActiveSupport inflections.humans.each { |(rule, replacement)| break if result.sub!(rule, replacement) } - result.sub!(/\A_+/, "".freeze) + result.sub!(/\A_+/, "") unless keep_id_suffix - result.sub!(/_id\z/, "".freeze) + result.sub!(/_id\z/, "") end - result.tr!("_".freeze, " ".freeze) + result.tr!("_", " ") result.gsub!(/([a-z\d]*)/i) do |match| "#{inflections.acronyms[match.downcase] || match.downcase}" @@ -200,14 +199,14 @@ module ActiveSupport # classify('calculus') # => "Calculus" def classify(table_name) # strip out any leading schema name - camelize(singularize(table_name.to_s.sub(/.*\./, "".freeze))) + camelize(singularize(table_name.to_s.sub(/.*\./, ""))) end # Replaces underscores with dashes in the string. # # dasherize('puni_puni') # => "puni-puni" def dasherize(underscored_word) - underscored_word.tr("_".freeze, "-".freeze) + underscored_word.tr("_", "-") end # Removes the module part from the expression in the string. @@ -270,7 +269,7 @@ module ActiveSupport # NameError is raised when the name is not in CamelCase or the constant is # unknown. def constantize(camel_cased_word) - names = camel_cased_word.split("::".freeze) + names = camel_cased_word.split("::") # Trigger a built-in NameError exception including the ill-formed constant in the message. Object.const_get(camel_cased_word) if names.empty? @@ -329,6 +328,8 @@ module ActiveSupport e.name.to_s == camel_cased_word.to_s) rescue ArgumentError => e raise unless /not missing constant #{const_regexp(camel_cased_word)}!$/.match?(e.message) + rescue LoadError => e + raise unless /Unable to autoload constant #{const_regexp(camel_cased_word)}/.match?(e.message) end # Returns the suffix that should be added to a number to denote the position @@ -341,18 +342,7 @@ module ActiveSupport # ordinal(-11) # => "th" # ordinal(-1021) # => "st" def ordinal(number) - abs_number = number.to_i.abs - - if (11..13).include?(abs_number % 100) - "th" - else - case abs_number % 10 - when 1; "st" - when 2; "nd" - when 3; "rd" - else "th" - end - end + I18n.translate("number.nth.ordinals", number: number) end # Turns a number into an ordinal string used to denote the position in an @@ -365,7 +355,7 @@ module ActiveSupport # ordinalize(-11) # => "-11th" # ordinalize(-1021) # => "-1021st" def ordinalize(number) - "#{number}#{ordinal(number)}" + I18n.translate("number.nth.ordinalized", number: number) end private @@ -376,7 +366,7 @@ module ActiveSupport # const_regexp("Foo::Bar::Baz") # => "Foo(::Bar(::Baz)?)?" # const_regexp("::") # => "::" def const_regexp(camel_cased_word) - parts = camel_cased_word.split("::".freeze) + parts = camel_cased_word.split("::") return Regexp.escape(camel_cased_word) if parts.blank? diff --git a/activesupport/lib/active_support/inflector/transliterate.rb b/activesupport/lib/active_support/inflector/transliterate.rb index 6f2ca4999c..0d2a17970f 100644 --- a/activesupport/lib/active_support/inflector/transliterate.rb +++ b/activesupport/lib/active_support/inflector/transliterate.rb @@ -58,13 +58,13 @@ module ActiveSupport # I18n.locale = :de # transliterate('Jürgen') # # => "Juergen" - def transliterate(string, replacement = "?".freeze) + def transliterate(string, replacement = "?") raise ArgumentError, "Can only transliterate strings. Received #{string.class.name}" unless string.is_a?(String) I18n.transliterate( - ActiveSupport::Multibyte::Unicode.normalize( - ActiveSupport::Multibyte::Unicode.tidy_bytes(string), :c), - replacement: replacement) + ActiveSupport::Multibyte::Unicode.tidy_bytes(string).unicode_normalize(:nfc), + replacement: replacement + ) end # Replaces special characters in a string so that it may be used as part of @@ -97,7 +97,7 @@ module ActiveSupport parameterized_string.gsub!(/[^a-z0-9\-_]+/i, separator) unless separator.nil? || separator.empty? - if separator == "-".freeze + if separator == "-" re_duplicate_separator = /-{2,}/ re_leading_trailing_separator = /^-|-$/i else @@ -108,7 +108,7 @@ module ActiveSupport # No more than one of the separator in a row. parameterized_string.gsub!(re_duplicate_separator, separator) # Remove leading/trailing separator. - parameterized_string.gsub!(re_leading_trailing_separator, "".freeze) + parameterized_string.gsub!(re_leading_trailing_separator, "") end parameterized_string.downcase! unless preserve_case diff --git a/activesupport/lib/active_support/json/decoding.rb b/activesupport/lib/active_support/json/decoding.rb index 8c0e016dc5..402a3fbe60 100644 --- a/activesupport/lib/active_support/json/decoding.rb +++ b/activesupport/lib/active_support/json/decoding.rb @@ -45,32 +45,32 @@ module ActiveSupport private - def convert_dates_from(data) - case data - when nil - nil - when DATE_REGEX - begin - Date.parse(data) - rescue ArgumentError + def convert_dates_from(data) + case data + when nil + nil + when DATE_REGEX + begin + Date.parse(data) + rescue ArgumentError + data + end + when DATETIME_REGEX + begin + Time.zone.parse(data) + rescue ArgumentError + data + end + when Array + data.map! { |d| convert_dates_from(d) } + when Hash + data.each do |key, value| + data[key] = convert_dates_from(value) + end + else data end - when DATETIME_REGEX - begin - Time.zone.parse(data) - rescue ArgumentError - data - end - when Array - data.map! { |d| convert_dates_from(d) } - when Hash - data.each do |key, value| - data[key] = convert_dates_from(value) - end - else - data end - end end end end diff --git a/activesupport/lib/active_support/json/encoding.rb b/activesupport/lib/active_support/json/encoding.rb index 1339c75ffe..de1b8ac8cf 100644 --- a/activesupport/lib/active_support/json/encoding.rb +++ b/activesupport/lib/active_support/json/encoding.rb @@ -54,9 +54,13 @@ module ActiveSupport class EscapedString < String #:nodoc: def to_json(*) if Encoding.escape_html_entities_in_json - super.gsub ESCAPE_REGEX_WITH_HTML_ENTITIES, ESCAPED_CHARS + s = super + s.gsub! ESCAPE_REGEX_WITH_HTML_ENTITIES, ESCAPED_CHARS + s else - super.gsub ESCAPE_REGEX_WITHOUT_HTML_ENTITIES, ESCAPED_CHARS + s = super + s.gsub! ESCAPE_REGEX_WITHOUT_HTML_ENTITIES, ESCAPED_CHARS + s end end diff --git a/activesupport/lib/active_support/key_generator.rb b/activesupport/lib/active_support/key_generator.rb index 78f7d7ca8d..8b61982883 100644 --- a/activesupport/lib/active_support/key_generator.rb +++ b/activesupport/lib/active_support/key_generator.rb @@ -38,36 +38,4 @@ module ActiveSupport @cache_keys[args.join] ||= @key_generator.generate_key(*args) end end - - class LegacyKeyGenerator # :nodoc: - SECRET_MIN_LENGTH = 30 # Characters - - def initialize(secret) - ensure_secret_secure(secret) - @secret = secret - end - - def generate_key(salt) - @secret - end - - private - - # To prevent users from using something insecure like "Password" we make sure that the - # secret they've provided is at least 30 characters in length. - def ensure_secret_secure(secret) - if secret.blank? - raise ArgumentError, "A secret is required to generate an integrity hash " \ - "for cookie session data. Set a secret_key_base of at least " \ - "#{SECRET_MIN_LENGTH} characters in via `bin/rails credentials:edit`." - end - - if secret.length < SECRET_MIN_LENGTH - raise ArgumentError, "Secret should be something secure, " \ - "like \"#{SecureRandom.hex(16)}\". The value you " \ - "provided, \"#{secret}\", is shorter than the minimum length " \ - "of #{SECRET_MIN_LENGTH} characters." - end - end - end end diff --git a/activesupport/lib/active_support/lazy_load_hooks.rb b/activesupport/lib/active_support/lazy_load_hooks.rb index dc8080c469..a6b096a973 100644 --- a/activesupport/lib/active_support/lazy_load_hooks.rb +++ b/activesupport/lib/active_support/lazy_load_hooks.rb @@ -68,7 +68,11 @@ module ActiveSupport if options[:yield] block.call(base) else - base.instance_eval(&block) + if base.is_a?(Module) + base.class_eval(&block) + else + base.instance_eval(&block) + end end end end diff --git a/activesupport/lib/active_support/locale/en.rb b/activesupport/lib/active_support/locale/en.rb new file mode 100644 index 0000000000..a2a7ea7ae1 --- /dev/null +++ b/activesupport/lib/active_support/locale/en.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +{ + en: { + number: { + nth: { + ordinals: lambda do |_key, number:, **_options| + case number + when 1; "st" + when 2; "nd" + when 3; "rd" + when 4, 5, 6, 7, 8, 9, 10, 11, 12, 13; "th" + else + num_modulo = number.to_i.abs % 100 + num_modulo %= 10 if num_modulo > 13 + case num_modulo + when 1; "st" + when 2; "nd" + when 3; "rd" + else "th" + end + end + end, + + ordinalized: lambda do |_key, number:, **_options| + "#{number}#{ActiveSupport::Inflector.ordinal(number)}" + end + } + } + } +} diff --git a/activesupport/lib/active_support/log_subscriber.rb b/activesupport/lib/active_support/log_subscriber.rb index 0f7be06c8e..938cfdb914 100644 --- a/activesupport/lib/active_support/log_subscriber.rb +++ b/activesupport/lib/active_support/log_subscriber.rb @@ -5,8 +5,8 @@ require "active_support/core_ext/class/attribute" require "active_support/subscriber" module ActiveSupport - # ActiveSupport::LogSubscriber is an object set to consume - # ActiveSupport::Notifications with the sole purpose of logging them. + # <tt>ActiveSupport::LogSubscriber</tt> is an object set to consume + # <tt>ActiveSupport::Notifications</tt> with the sole purpose of logging them. # The log subscriber dispatches notifications to a registered object based # on its given namespace. # @@ -16,7 +16,7 @@ module ActiveSupport # module ActiveRecord # class LogSubscriber < ActiveSupport::LogSubscriber # def sql(event) - # "#{event.payload[:name]} (#{event.duration}) #{event.payload[:sql]}" + # info "#{event.payload[:name]} (#{event.duration}) #{event.payload[:sql]}" # end # end # end @@ -29,13 +29,36 @@ module ActiveSupport # subscriber, the line above should be called after your # <tt>ActiveRecord::LogSubscriber</tt> definition. # - # After configured, whenever a "sql.active_record" notification is published, - # it will properly dispatch the event (ActiveSupport::Notifications::Event) to - # the sql method. + # After configured, whenever a <tt>"sql.active_record"</tt> notification is published, + # it will properly dispatch the event + # (<tt>ActiveSupport::Notifications::Event</tt>) to the sql method. + # + # Being an <tt>ActiveSupport::Notifications</tt> consumer, + # <tt>ActiveSupport::LogSubscriber</tt> exposes a simple interface to check if + # instrumented code raises an exception. It is common to log a different + # message in case of an error, and this can be achieved by extending + # the previous example: + # + # module ActiveRecord + # class LogSubscriber < ActiveSupport::LogSubscriber + # def sql(event) + # exception = event.payload[:exception] + # + # if exception + # exception_object = event.payload[:exception_object] + # + # error "[ERROR] #{event.payload[:name]}: #{exception.join(', ')} " \ + # "(#{exception_object.backtrace.first})" + # else + # # standard logger code + # end + # end + # end + # end # # Log subscriber also has some helpers to deal with logging and automatically - # flushes all logs when the request finishes (via action_dispatch.callback - # notification) in a Rails environment. + # flushes all logs when the request finishes + # (via <tt>action_dispatch.callback</tt> notification) in a Rails environment. class LogSubscriber < Subscriber # Embed in a String to clear all previous ANSI sequences. CLEAR = "\e[0m" diff --git a/activesupport/lib/active_support/logger.rb b/activesupport/lib/active_support/logger.rb index 8152a182b4..b8555c887b 100644 --- a/activesupport/lib/active_support/logger.rb +++ b/activesupport/lib/active_support/logger.rb @@ -6,7 +6,6 @@ require "logger" module ActiveSupport class Logger < ::Logger - include ActiveSupport::LoggerThreadSafeLevel include LoggerSilence # Returns true if the logger destination matches one of the sources @@ -81,20 +80,6 @@ module ActiveSupport def initialize(*args) super @formatter = SimpleFormatter.new - after_initialize if respond_to? :after_initialize - end - - def add(severity, message = nil, progname = nil, &block) - return true if @logdev.nil? || (severity || UNKNOWN) < level - super - end - - Logger::Severity.constants.each do |severity| - class_eval(<<-EOT, __FILE__, __LINE__ + 1) - def #{severity.downcase}? # def debug? - Logger::#{severity} >= level # DEBUG >= level - end # end - EOT end # Simple formatter which only displays the message. diff --git a/activesupport/lib/active_support/logger_silence.rb b/activesupport/lib/active_support/logger_silence.rb index 89f32b6782..b2444c1e34 100644 --- a/activesupport/lib/active_support/logger_silence.rb +++ b/activesupport/lib/active_support/logger_silence.rb @@ -2,28 +2,44 @@ require "active_support/concern" require "active_support/core_ext/module/attribute_accessors" -require "concurrent" +require "active_support/logger_thread_safe_level" module LoggerSilence extend ActiveSupport::Concern included do - cattr_accessor :silencer, default: true + ActiveSupport::Deprecation.warn( + "Including LoggerSilence is deprecated and will be removed in Rails 6.1. " \ + "Please use `ActiveSupport::LoggerSilence` instead" + ) + + include ActiveSupport::LoggerSilence end +end + +module ActiveSupport + module LoggerSilence + extend ActiveSupport::Concern + + included do + cattr_accessor :silencer, default: true + include ActiveSupport::LoggerThreadSafeLevel + end - # Silences the logger for the duration of the block. - def silence(temporary_level = Logger::ERROR) - if silencer - begin - old_local_level = local_level - self.local_level = temporary_level + # Silences the logger for the duration of the block. + def silence(temporary_level = Logger::ERROR) + if silencer + begin + old_local_level = local_level + self.local_level = temporary_level + yield self + ensure + self.local_level = old_local_level + end + else yield self - ensure - self.local_level = old_local_level end - else - yield self end end end diff --git a/activesupport/lib/active_support/logger_thread_safe_level.rb b/activesupport/lib/active_support/logger_thread_safe_level.rb index ba32813d3d..f16c90cfc6 100644 --- a/activesupport/lib/active_support/logger_thread_safe_level.rb +++ b/activesupport/lib/active_support/logger_thread_safe_level.rb @@ -1,13 +1,30 @@ # frozen_string_literal: true require "active_support/concern" +require "active_support/core_ext/module/attribute_accessors" +require "concurrent" module ActiveSupport module LoggerThreadSafeLevel # :nodoc: extend ActiveSupport::Concern + included do + cattr_accessor :local_levels, default: Concurrent::Map.new(initial_capacity: 2), instance_accessor: false + end + + Logger::Severity.constants.each do |severity| + class_eval(<<-EOT, __FILE__, __LINE__ + 1) + def #{severity.downcase}? # def debug? + Logger::#{severity} >= level # DEBUG >= level + end # end + EOT + end + def after_initialize - @local_levels = Concurrent::Map.new(initial_capacity: 2) + ActiveSupport::Deprecation.warn( + "Logger don't need to call #after_initialize directly anymore. It will be deprecated without replacement in " \ + "Rails 6.1." + ) end def local_log_id @@ -15,19 +32,24 @@ module ActiveSupport end def local_level - @local_levels[local_log_id] + self.class.local_levels[local_log_id] end def local_level=(level) if level - @local_levels[local_log_id] = level + self.class.local_levels[local_log_id] = level else - @local_levels.delete(local_log_id) + self.class.local_levels.delete(local_log_id) end end def level local_level || super end + + def add(severity, message = nil, progname = nil, &block) # :nodoc: + return true if @logdev.nil? || (severity || UNKNOWN) < level + super + end end end diff --git a/activesupport/lib/active_support/message_encryptor.rb b/activesupport/lib/active_support/message_encryptor.rb index 27fd061947..7d6f8937f0 100644 --- a/activesupport/lib/active_support/message_encryptor.rb +++ b/activesupport/lib/active_support/message_encryptor.rb @@ -3,6 +3,7 @@ require "openssl" require "base64" require "active_support/core_ext/array/extract_options" +require "active_support/core_ext/module/attribute_accessors" require "active_support/message_verifier" require "active_support/messages/metadata" @@ -52,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 @@ -81,9 +82,9 @@ module ActiveSupport class MessageEncryptor prepend Messages::Rotator::Encryptor - class << self - attr_accessor :use_authenticated_message_encryption #:nodoc: + cattr_accessor :use_authenticated_message_encryption, instance_accessor: false, default: false + class << self def default_cipher #:nodoc: if use_authenticated_message_encryption "aes-256-gcm" @@ -181,7 +182,7 @@ module ActiveSupport def _decrypt(encrypted_message, purpose) cipher = new_cipher - encrypted_data, iv, auth_tag = encrypted_message.split("--".freeze).map { |v| ::Base64.strict_decode64(v) } + encrypted_data, iv, auth_tag = encrypted_message.split("--").map { |v| ::Base64.strict_decode64(v) } # Currently the OpenSSL bindings do not raise an error if auth_tag is # truncated, which would allow an attacker to easily forge it. See @@ -209,9 +210,7 @@ module ActiveSupport OpenSSL::Cipher.new(@cipher) end - def verifier - @verifier - end + attr_reader :verifier def aead_mode? @aead_mode ||= new_cipher.authenticated? diff --git a/activesupport/lib/active_support/message_verifier.rb b/activesupport/lib/active_support/message_verifier.rb index 83c39c0a86..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>. # @@ -122,7 +122,7 @@ module ActiveSupport def valid_message?(signed_message) return if signed_message.nil? || !signed_message.valid_encoding? || signed_message.blank? - data, digest = signed_message.split("--".freeze) + data, digest = signed_message.split("--") data.present? && digest.present? && ActiveSupport::SecurityUtils.secure_compare(digest, generate_digest(data)) end @@ -150,7 +150,7 @@ module ActiveSupport def verified(signed_message, purpose: nil, **) if valid_message?(signed_message) begin - data = signed_message.split("--".freeze)[0] + data = signed_message.split("--")[0] message = Messages::Metadata.verify(decode(data), purpose) @serializer.load(message) if message rescue ArgumentError => argument_error diff --git a/activesupport/lib/active_support/multibyte/chars.rb b/activesupport/lib/active_support/multibyte/chars.rb index 8152b8fd22..a1e23aeaca 100644 --- a/activesupport/lib/active_support/multibyte/chars.rb +++ b/activesupport/lib/active_support/multibyte/chars.rb @@ -4,7 +4,6 @@ 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" module ActiveSupport #:nodoc: module Multibyte #:nodoc: @@ -18,7 +17,7 @@ module ActiveSupport #:nodoc: # through the +mb_chars+ method. Methods which would normally return a # String object now return a Chars object so methods can be chained. # - # 'The Perfect String '.mb_chars.downcase.strip.normalize + # 'The Perfect String '.mb_chars.downcase.strip # # => #<ActiveSupport::Multibyte::Chars:0x007fdc434ccc10 @wrapped_string="the perfect string"> # # Chars objects are perfectly interchangeable with String objects as long as @@ -77,6 +76,11 @@ module ActiveSupport #:nodoc: # Returns +true+ when the proxy class can handle the string. Returns # +false+ otherwise. def self.consumes?(string) + ActiveSupport::Deprecation.warn(<<-MSG.squish) + ActiveSupport::Multibyte::Chars.consumes? is deprecated and will be + removed from Rails 6.1. Use string.is_utf8? instead. + MSG + string.encoding == Encoding::UTF_8 end @@ -109,7 +113,7 @@ module ActiveSupport #:nodoc: # # 'Café'.mb_chars.reverse.to_s # => 'éfaC' def reverse - chars(Unicode.unpack_graphemes(@wrapped_string).reverse.flatten.pack("U*")) + chars(@wrapped_string.scan(/\X/).reverse.join) end # Limits the byte size of the string to a number of bytes without breaking @@ -118,35 +122,7 @@ module ActiveSupport #:nodoc: # # 'こんにちは'.mb_chars.limit(7).to_s # => "こん" def limit(limit) - slice(0...translate_offset(limit)) - end - - # Converts characters in the string to uppercase. - # - # 'Laurent, où sont les tests ?'.mb_chars.upcase.to_s # => "LAURENT, OÙ SONT LES TESTS ?" - def upcase - chars Unicode.upcase(@wrapped_string) - end - - # Converts characters in the string to lowercase. - # - # 'VĚDA A VÝZKUM'.mb_chars.downcase.to_s # => "věda a výzkum" - def downcase - chars Unicode.downcase(@wrapped_string) - end - - # Converts characters in the string to the opposite case. - # - # 'El Cañón'.mb_chars.swapcase.to_s # => "eL cAÑÓN" - def swapcase - chars Unicode.swapcase(@wrapped_string) - end - - # Converts the first character to uppercase and the remainder to lowercase. - # - # 'über'.mb_chars.capitalize.to_s # => "Über" - def capitalize - (slice(0) || chars("")).upcase + (slice(1..-1) || chars("")).downcase + truncate_bytes(limit, omission: nil) end # Capitalizes the first letter of every word, when possible. @@ -154,7 +130,7 @@ module ActiveSupport #:nodoc: # "ÉL QUE SE ENTERÓ".mb_chars.titleize.to_s # => "Él Que Se Enteró" # "日本語".mb_chars.titleize.to_s # => "日本語" def titleize - chars(downcase.to_s.gsub(/\b('?\S)/u) { Unicode.upcase($1) }) + chars(downcase.to_s.gsub(/\b('?\S)/u) { $1.upcase }) end alias_method :titlecase, :titleize @@ -166,7 +142,24 @@ module ActiveSupport #:nodoc: # <tt>:c</tt>, <tt>:kc</tt>, <tt>:d</tt>, or <tt>:kd</tt>. Default is # ActiveSupport::Multibyte::Unicode.default_normalization_form def normalize(form = nil) - chars(Unicode.normalize(@wrapped_string, form)) + form ||= Unicode.default_normalization_form + + # See https://www.unicode.org/reports/tr15, Table 1 + if alias_form = Unicode::NORMALIZATION_FORM_ALIASES[form] + ActiveSupport::Deprecation.warn(<<-MSG.squish) + ActiveSupport::Multibyte::Chars#normalize is deprecated and will be + removed from Rails 6.1. Use #unicode_normalize(:#{alias_form}) instead. + MSG + + send(:unicode_normalize, alias_form) + else + ActiveSupport::Deprecation.warn(<<-MSG.squish) + ActiveSupport::Multibyte::Chars#normalize is deprecated and will be + removed from Rails 6.1. Use #unicode_normalize instead. + MSG + + raise ArgumentError, "#{form} is not a valid normalization variant", caller + end end # Performs canonical decomposition on all the characters. @@ -190,7 +183,7 @@ module ActiveSupport #:nodoc: # 'क्षि'.mb_chars.length # => 4 # 'क्षि'.mb_chars.grapheme_length # => 3 def grapheme_length - Unicode.unpack_graphemes(@wrapped_string).length + @wrapped_string.scan(/\X/).length end # Replaces all ISO-8859-1 or CP1252 characters by their UTF-8 equivalent @@ -206,7 +199,7 @@ module ActiveSupport #:nodoc: to_s.as_json(options) end - %w(capitalize downcase reverse tidy_bytes upcase).each do |method| + %w(reverse tidy_bytes).each do |method| define_method("#{method}!") do |*args| @wrapped_string = send(method, *args).to_s self @@ -215,18 +208,6 @@ module ActiveSupport #:nodoc: private - def translate_offset(byte_offset) - return nil if byte_offset.nil? - return 0 if @wrapped_string == "" - - begin - @wrapped_string.byteslice(0...byte_offset).unpack("U*").length - rescue ArgumentError - byte_offset -= 1 - retry - end - end - def chars(string) self.class.new(string) end diff --git a/activesupport/lib/active_support/multibyte/unicode.rb b/activesupport/lib/active_support/multibyte/unicode.rb index a64223c0e0..ce8ecece69 100644 --- a/activesupport/lib/active_support/multibyte/unicode.rb +++ b/activesupport/lib/active_support/multibyte/unicode.rb @@ -6,12 +6,19 @@ module ActiveSupport extend self # A list of all available normalization forms. - # See http://www.unicode.org/reports/tr15/tr15-29.html for more + # See https://www.unicode.org/reports/tr15/tr15-29.html for more # information about normalization. NORMALIZATION_FORMS = [:c, :kc, :d, :kd] + NORMALIZATION_FORM_ALIASES = { # :nodoc: + c: :nfc, + d: :nfd, + kc: :nfkc, + kd: :nfkd + } + # The Unicode version that is supported by the implementation - UNICODE_VERSION = "9.0.0" + UNICODE_VERSION = RbConfig::CONFIG["UNICODE_VERSION"] # The default normalization used for operations that require # normalization. It can be set to any of the normalizations @@ -21,199 +28,44 @@ module ActiveSupport attr_accessor :default_normalization_form @default_normalization_form = :kc - # Hangul character boundaries and properties - HANGUL_SBASE = 0xAC00 - HANGUL_LBASE = 0x1100 - HANGUL_VBASE = 0x1161 - HANGUL_TBASE = 0x11A7 - HANGUL_LCOUNT = 19 - HANGUL_VCOUNT = 21 - HANGUL_TCOUNT = 28 - HANGUL_NCOUNT = HANGUL_VCOUNT * HANGUL_TCOUNT - HANGUL_SCOUNT = 11172 - HANGUL_SLAST = HANGUL_SBASE + HANGUL_SCOUNT - - # Detect whether the codepoint is in a certain character class. Returns - # +true+ when it's in the specified character class and +false+ otherwise. - # Valid character classes are: <tt>:cr</tt>, <tt>:lf</tt>, <tt>:l</tt>, - # <tt>:v</tt>, <tt>:lv</tt>, <tt>:lvt</tt> and <tt>:t</tt>. - # - # Primarily used by the grapheme cluster support. - def in_char_class?(codepoint, classes) - classes.detect { |c| database.boundary[c] === codepoint } ? true : false - end - # Unpack the string at grapheme boundaries. Returns a list of character # lists. # # Unicode.unpack_graphemes('क्षि') # => [[2325, 2381], [2359], [2367]] # Unicode.unpack_graphemes('Café') # => [[67], [97], [102], [233]] def unpack_graphemes(string) - codepoints = string.codepoints.to_a - unpacked = [] - pos = 0 - marker = 0 - eoc = codepoints.length - while (pos < eoc) - pos += 1 - previous = codepoints[pos - 1] - current = codepoints[pos] - - # See http://unicode.org/reports/tr29/#Grapheme_Cluster_Boundary_Rules - should_break = - if pos == eoc - true - # GB3. CR X LF - elsif previous == database.boundary[:cr] && current == database.boundary[:lf] - false - # GB4. (Control|CR|LF) ÷ - elsif previous && in_char_class?(previous, [:control, :cr, :lf]) - true - # GB5. ÷ (Control|CR|LF) - elsif in_char_class?(current, [:control, :cr, :lf]) - true - # GB6. L X (L|V|LV|LVT) - elsif database.boundary[:l] === previous && in_char_class?(current, [:l, :v, :lv, :lvt]) - false - # GB7. (LV|V) X (V|T) - elsif in_char_class?(previous, [:lv, :v]) && in_char_class?(current, [:v, :t]) - false - # GB8. (LVT|T) X (T) - elsif in_char_class?(previous, [:lvt, :t]) && database.boundary[:t] === current - false - # GB9. X (Extend | ZWJ) - elsif in_char_class?(current, [:extend, :zwj]) - false - # GB9a. X SpacingMark - elsif database.boundary[:spacingmark] === current - false - # GB9b. Prepend X - elsif database.boundary[:prepend] === previous - false - # GB10. (E_Base | EBG) Extend* X E_Modifier - elsif (marker...pos).any? { |i| in_char_class?(codepoints[i], [:e_base, :e_base_gaz]) && codepoints[i + 1...pos].all? { |c| database.boundary[:extend] === c } } && database.boundary[:e_modifier] === current - false - # GB11. ZWJ X (Glue_After_Zwj | EBG) - elsif database.boundary[:zwj] === previous && in_char_class?(current, [:glue_after_zwj, :e_base_gaz]) - false - # GB12. ^ (RI RI)* RI X RI - # GB13. [^RI] (RI RI)* RI X RI - elsif codepoints[marker..pos].all? { |c| database.boundary[:regional_indicator] === c } && codepoints[marker..pos].count { |c| database.boundary[:regional_indicator] === c }.even? - false - # GB999. Any ÷ Any - else - true - end + ActiveSupport::Deprecation.warn(<<-MSG.squish) + ActiveSupport::Multibyte::Unicode#unpack_graphemes is deprecated and will be + removed from Rails 6.1. Use string.scan(/\X/).map(&:codepoints) instead. + MSG - if should_break - unpacked << codepoints[marker..pos - 1] - marker = pos - end - end - unpacked + string.scan(/\X/).map(&:codepoints) end # Reverse operation of unpack_graphemes. # # Unicode.pack_graphemes(Unicode.unpack_graphemes('क्षि')) # => 'क्षि' def pack_graphemes(unpacked) - unpacked.flatten.pack("U*") - end + ActiveSupport::Deprecation.warn(<<-MSG.squish) + ActiveSupport::Multibyte::Unicode#pack_graphemes is deprecated and will be + removed from Rails 6.1. Use array.flatten.pack("U*") instead. + MSG - # Re-order codepoints so the string becomes canonical. - def reorder_characters(codepoints) - length = codepoints.length - 1 - pos = 0 - while pos < length do - cp1, cp2 = database.codepoints[codepoints[pos]], database.codepoints[codepoints[pos + 1]] - if (cp1.combining_class > cp2.combining_class) && (cp2.combining_class > 0) - codepoints[pos..pos + 1] = cp2.code, cp1.code - pos += (pos > 0 ? -1 : 1) - else - pos += 1 - end - end - codepoints + unpacked.flatten.pack("U*") end # Decompose composed characters to the decomposed form. def decompose(type, codepoints) - codepoints.inject([]) do |decomposed, cp| - # if it's a hangul syllable starter character - if HANGUL_SBASE <= cp && cp < HANGUL_SLAST - sindex = cp - HANGUL_SBASE - ncp = [] # new codepoints - ncp << HANGUL_LBASE + sindex / HANGUL_NCOUNT - ncp << HANGUL_VBASE + (sindex % HANGUL_NCOUNT) / HANGUL_TCOUNT - tindex = sindex % HANGUL_TCOUNT - ncp << (HANGUL_TBASE + tindex) unless tindex == 0 - decomposed.concat ncp - # if the codepoint is decomposable in with the current decomposition type - elsif (ncp = database.codepoints[cp].decomp_mapping) && (!database.codepoints[cp].decomp_type || type == :compatibility) - decomposed.concat decompose(type, ncp.dup) - else - decomposed << cp - end + if type == :compatibility + codepoints.pack("U*").unicode_normalize(:nfkd).codepoints + else + codepoints.pack("U*").unicode_normalize(:nfd).codepoints end end # Compose decomposed characters to the composed form. def compose(codepoints) - pos = 0 - eoa = codepoints.length - 1 - starter_pos = 0 - starter_char = codepoints[0] - previous_combining_class = -1 - while pos < eoa - pos += 1 - lindex = starter_char - HANGUL_LBASE - # -- Hangul - if 0 <= lindex && lindex < HANGUL_LCOUNT - vindex = codepoints[starter_pos + 1] - HANGUL_VBASE rescue vindex = -1 - if 0 <= vindex && vindex < HANGUL_VCOUNT - tindex = codepoints[starter_pos + 2] - HANGUL_TBASE rescue tindex = -1 - if 0 <= tindex && tindex < HANGUL_TCOUNT - j = starter_pos + 2 - eoa -= 2 - else - tindex = 0 - j = starter_pos + 1 - eoa -= 1 - end - codepoints[starter_pos..j] = (lindex * HANGUL_VCOUNT + vindex) * HANGUL_TCOUNT + tindex + HANGUL_SBASE - end - starter_pos += 1 - starter_char = codepoints[starter_pos] - # -- Other characters - else - current_char = codepoints[pos] - current = database.codepoints[current_char] - if current.combining_class > previous_combining_class - if ref = database.composition_map[starter_char] - composition = ref[current_char] - else - composition = nil - end - unless composition.nil? - codepoints[starter_pos] = composition - starter_char = composition - codepoints.delete_at pos - eoa -= 1 - pos -= 1 - previous_combining_class = -1 - else - previous_combining_class = current.combining_class - end - else - previous_combining_class = current.combining_class - end - if current.combining_class == 0 - starter_pos = pos - starter_char = codepoints[pos] - end - end - end - codepoints + codepoints.pack("U*").unicode_normalize(:nfc).codepoints end # Rubinius' String#scrub, however, doesn't support ASCII-incompatible chars. @@ -265,130 +117,41 @@ module ActiveSupport # Default is ActiveSupport::Multibyte::Unicode.default_normalization_form. def normalize(string, form = nil) form ||= @default_normalization_form - # See http://www.unicode.org/reports/tr15, Table 1 - codepoints = string.codepoints.to_a - case form - when :d - reorder_characters(decompose(:canonical, codepoints)) - when :c - compose(reorder_characters(decompose(:canonical, codepoints))) - when :kd - reorder_characters(decompose(:compatibility, codepoints)) - when :kc - compose(reorder_characters(decompose(:compatibility, codepoints))) - else - raise ArgumentError, "#{form} is not a valid normalization variant", caller - end.pack("U*".freeze) - end - - def downcase(string) - apply_mapping string, :lowercase_mapping - end - - def upcase(string) - apply_mapping string, :uppercase_mapping - end - def swapcase(string) - apply_mapping string, :swapcase_mapping - end - - # Holds data about a codepoint in the Unicode database. - class Codepoint - attr_accessor :code, :combining_class, :decomp_type, :decomp_mapping, :uppercase_mapping, :lowercase_mapping + # See https://www.unicode.org/reports/tr15, Table 1 + if alias_form = NORMALIZATION_FORM_ALIASES[form] + ActiveSupport::Deprecation.warn(<<-MSG.squish) + ActiveSupport::Multibyte::Unicode#normalize is deprecated and will be + removed from Rails 6.1. Use String#unicode_normalize(:#{alias_form}) instead. + MSG - # Initializing Codepoint object with default values - def initialize - @combining_class = 0 - @uppercase_mapping = 0 - @lowercase_mapping = 0 - end + string.unicode_normalize(alias_form) + else + ActiveSupport::Deprecation.warn(<<-MSG.squish) + ActiveSupport::Multibyte::Unicode#normalize is deprecated and will be + removed from Rails 6.1. Use String#unicode_normalize instead. + MSG - def swapcase_mapping - uppercase_mapping > 0 ? uppercase_mapping : lowercase_mapping + raise ArgumentError, "#{form} is not a valid normalization variant", caller end end - # Holds static data from the Unicode database. - class UnicodeDatabase - ATTRIBUTES = :codepoints, :composition_exclusion, :composition_map, :boundary, :cp1252 - - attr_writer(*ATTRIBUTES) - - def initialize - @codepoints = Hash.new(Codepoint.new) - @composition_exclusion = [] - @composition_map = {} - @boundary = {} - @cp1252 = {} - end - - # Lazy load the Unicode database so it's only loaded when it's actually used - ATTRIBUTES.each do |attr_name| - class_eval(<<-EOS, __FILE__, __LINE__ + 1) - def #{attr_name} # def codepoints - load # load - @#{attr_name} # @codepoints - end # end - EOS - end - - # Loads the Unicode database and returns all the internal objects of - # UnicodeDatabase. - def load - begin - @codepoints, @composition_exclusion, @composition_map, @boundary, @cp1252 = File.open(self.class.filename, "rb") { |f| Marshal.load f.read } - rescue => e - raise IOError.new("Couldn't load the Unicode tables for UTF8Handler (#{e.message}), ActiveSupport::Multibyte is unusable") - end - - # Redefine the === method so we can write shorter rules for grapheme cluster breaks - @boundary.each_key do |k| - @boundary[k].instance_eval do - def ===(other) - detect { |i| i === other } ? true : false - end - end if @boundary[k].kind_of?(Array) - end - - # define attr_reader methods for the instance variables - class << self - attr_reader(*ATTRIBUTES) - end - end + %w(downcase upcase swapcase).each do |method| + define_method(method) do |string| + ActiveSupport::Deprecation.warn(<<-MSG.squish) + ActiveSupport::Multibyte::Unicode##{method} is deprecated and + will be removed from Rails 6.1. Use String methods directly. + MSG - # Returns the directory in which the data files are stored. - def self.dirname - File.expand_path("../values", __dir__) - end - - # Returns the filename for the data file for this version. - def self.filename - File.expand_path File.join(dirname, "unicode_tables.dat") + string.send(method) end end private - def apply_mapping(string, mapping) - database.codepoints - string.each_codepoint.map do |codepoint| - cp = database.codepoints[codepoint] - if cp && (ncp = cp.send(mapping)) && ncp > 0 - ncp - else - codepoint - end - end.pack("U*") - end - def recode_windows1252_chars(string) string.encode(Encoding::UTF_8, Encoding::Windows_1252, invalid: :replace, undef: :replace) end - - def database - @database ||= UnicodeDatabase.new - end end end end diff --git a/activesupport/lib/active_support/notifications.rb b/activesupport/lib/active_support/notifications.rb index 6207de8094..d9e93b530c 100644 --- a/activesupport/lib/active_support/notifications.rb +++ b/activesupport/lib/active_support/notifications.rb @@ -34,7 +34,7 @@ module ActiveSupport # name # => String, name of the event (such as 'render' from above) # start # => Time, when the instrumented block started execution # finish # => Time, when the instrumented block ended execution - # id # => String, unique ID for this notification + # id # => String, unique ID for the instrumenter that fired the event # payload # => Hash, the payload # end # @@ -59,7 +59,7 @@ module ActiveSupport # event.payload # => { extra: :information } # # The block in the <tt>subscribe</tt> call gets the name of the event, start - # timestamp, end timestamp, a string with a unique identifier for that event + # timestamp, end timestamp, a string with a unique identifier for that event's instrumenter # (something like "535801666f04d0298cd6"), and a hash with the payload, in # that order. # @@ -67,9 +67,12 @@ module ActiveSupport # 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. + # itself as the value: # - # As the previous example depicts, the class <tt>ActiveSupport::Notifications::Event</tt> + # event.payload[:exception] # => ["ArgumentError", "Invalid value"] + # event.payload[:exception_object] # => #<ArgumentError: Invalid value> + # + # As the earlier example depicts, the class <tt>ActiveSupport::Notifications::Event</tt> # is able to take the arguments as they come and provide an object-oriented # interface to that data. # @@ -150,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 @@ -171,6 +183,31 @@ module ActiveSupport end end + # Subscribe to a given event name with the passed +block+. + # + # You can subscribe to events by passing a String to match exact event + # names, or by passing a Regexp to match all events that match a pattern. + # + # ActiveSupport::Notifications.subscribe(/render/) do |*args| + # @event = ActiveSupport::Notifications::Event.new(*args) + # end + # + # The +block+ will receive five parameters with information about the event: + # + # ActiveSupport::Notifications.subscribe('render') do |name, start, finish, id, payload| + # name # => String, name of the event (such as 'render' from above) + # start # => Time, when the instrumented block started execution + # finish # => Time, when the instrumented block ended execution + # id # => String, unique ID for the instrumenter that fired the event + # payload # => Hash, the payload + # end + # + # If the block passed to the method only takes one parameter, + # it will yield an event object to the block: + # + # ActiveSupport::Notifications.subscribe(/render/) do |event| + # @event = event + # end def subscribe(*args, &block) notifier.subscribe(*args, &block) end diff --git a/activesupport/lib/active_support/notifications/fanout.rb b/activesupport/lib/active_support/notifications/fanout.rb index 25aab175b4..f06504cf2c 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,7 +14,8 @@ 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 @@ -21,8 +23,13 @@ module ActiveSupport def subscribe(pattern = nil, block = Proc.new) subscriber = Subscribers.new pattern, 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 @@ -70,12 +85,29 @@ module ActiveSupport module Subscribers # :nodoc: def self.new(pattern, listener) + subscriber_class = Timed + if listener.respond_to?(:start) && listener.respond_to?(:finish) - subscriber = Evented.new pattern, listener + subscriber_class = Evented else - subscriber = Timed.new pattern, listener + # Doing all this to detect a block like `proc { |x| }` vs + # `proc { |*x| }` or `proc { |**x| }` + if listener.respond_to?(:parameters) + params = listener.parameters + if params.length == 1 && params.first.first == :opt + subscriber_class = EventObject + end + end end + wrap_all pattern, subscriber_class.new(pattern, listener) + end + + def self.event_object_subscriber(pattern, block) + wrap_all pattern, EventObject.new(pattern, block) + end + + def self.wrap_all(pattern, subscriber) unless pattern AllMessages.new(subscriber) else @@ -83,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 @@ -105,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 @@ -130,6 +190,27 @@ module ActiveSupport end end + class EventObject < Evented + def start(name, id, payload) + stack = Thread.current[:_event_stack] ||= [] + event = build_event name, id, payload + event.start! + stack.push event + end + + def finish(name, id, payload) + stack = Thread.current[:_event_stack] + event = stack.pop + event.finish! + @delegate.call event + end + + private + def build_event(name, id, payload) + ActiveSupport::Notifications::Event.new name, nil, nil, id, payload + end + end + class AllMessages # :nodoc: def initialize(delegate) @delegate = delegate @@ -151,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 e99f5ee688..00a57c38c9 100644 --- a/activesupport/lib/active_support/notifications/instrumenter.rb +++ b/activesupport/lib/active_support/notifications/instrumenter.rb @@ -52,8 +52,13 @@ module ActiveSupport end class Event - attr_reader :name, :time, :transaction_id, :payload, :children - attr_accessor :end + attr_reader :name, :time, :end, :transaction_id, :payload, :children + + def self.clock_gettime_supported? # :nodoc: + defined?(Process::CLOCK_PROCESS_CPUTIME_ID) && + !Gem.win_platform? + end + private_class_method :clock_gettime_supported? def initialize(name, start, ending, transaction_id, payload) @name = name @@ -63,6 +68,47 @@ module ActiveSupport @end = ending @children = [] @duration = nil + @cpu_time_start = nil + @cpu_time_finish = nil + @allocation_count_start = 0 + @allocation_count_finish = 0 + end + + # Record information at the time this event starts + def start! + @time = now + @cpu_time_start = now_cpu + @allocation_count_start = now_allocations + end + + # Record information at the time this event finishes + def finish! + @cpu_time_finish = now_cpu + @end = now + @allocation_count_finish = now_allocations + end + + def end=(ending) + ActiveSupport::Deprecation.deprecation_warning(:end=, :finish!) + @end = ending + end + + # Returns the CPU time (in milliseconds) passed since the call to + # +start!+ and the call to +finish!+ + def cpu_time + (@cpu_time_finish - @cpu_time_start) * 1000 + end + + # Returns the idle time time (in milliseconds) passed since the call to + # +start!+ and the call to +finish!+ + def idle_time + duration - cpu_time + end + + # Returns the number of allocations made since the call to +start!+ and + # the call to +finish!+ + def allocations + @allocation_count_finish - @allocation_count_start end # Returns the difference in milliseconds between when the execution of the @@ -88,6 +134,31 @@ module ActiveSupport def parent_of?(event) @children.include? event end + + private + def now + Concurrent.monotonic_time + end + + if clock_gettime_supported? + def now_cpu + Process.clock_gettime(Process::CLOCK_PROCESS_CPUTIME_ID) + end + else + def now_cpu + 0 + end + end + + if defined?(JRUBY_VERSION) + def now_allocations + 0 + end + else + def now_allocations + GC.stat :total_allocated_objects + end + end end end end diff --git a/activesupport/lib/active_support/number_helper.rb b/activesupport/lib/active_support/number_helper.rb index 8fd6e932f1..d19a2f64d4 100644 --- a/activesupport/lib/active_support/number_helper.rb +++ b/activesupport/lib/active_support/number_helper.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require "active_support/dependencies/autoload" + module ActiveSupport module NumberHelper extend ActiveSupport::Autoload @@ -85,6 +87,9 @@ module ActiveSupport # number given by <tt>:format</tt>). Accepts the same fields # than <tt>:format</tt>, except <tt>%n</tt> is here the # absolute value of the number. + # * <tt>:strip_insignificant_zeros</tt> - If +true+ removes + # insignificant zeros after the decimal separator (defaults to + # +false+). # # ==== Examples # @@ -100,6 +105,8 @@ module ActiveSupport # # => "£1234567890,50" # number_to_currency(1234567890.50, unit: '£', separator: ',', delimiter: '', format: '%n %u') # # => "1234567890,50 £" + # number_to_currency(1234567890.50, strip_insignificant_zeros: true) + # # => "$1,234,567,890.5" def number_to_currency(number, options = {}) NumberToCurrencyConverter.convert(number, options) end 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 3f037c73ed..0e8ae82dd5 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,6 +1,6 @@ # frozen_string_literal: true -require "active_support/core_ext/numeric/inquiry" +require "active_support/number_helper/number_converter" module ActiveSupport module NumberHelper @@ -17,7 +17,7 @@ module ActiveSupport end rounded_number = NumberToRoundedConverter.convert(number, options) - format.gsub("%n".freeze, rounded_number).gsub("%u".freeze, options[:unit]) + format.gsub("%n", rounded_number).gsub("%u", options[:unit]) end private diff --git a/activesupport/lib/active_support/number_helper/number_to_delimited_converter.rb b/activesupport/lib/active_support/number_helper/number_to_delimited_converter.rb index d5b5706705..467a580a2e 100644 --- a/activesupport/lib/active_support/number_helper/number_to_delimited_converter.rb +++ b/activesupport/lib/active_support/number_helper/number_to_delimited_converter.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require "active_support/number_helper/number_converter" + module ActiveSupport module NumberHelper class NumberToDelimitedConverter < NumberConverter #:nodoc: @@ -14,7 +16,7 @@ module ActiveSupport private def parts - left, right = number.to_s.split(".".freeze) + left, right = number.to_s.split(".") left.gsub!(delimiter_pattern) do |digit_to_delimit| "#{digit_to_delimit}#{options[:delimiter]}" end 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 03eb6671ec..494408fc01 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 @@ -1,5 +1,7 @@ # frozen_string_literal: true +require "active_support/number_helper/number_converter" + module ActiveSupport module NumberHelper class NumberToHumanConverter < NumberConverter # :nodoc: @@ -25,7 +27,7 @@ module ActiveSupport rounded_number = NumberToRoundedConverter.convert(number, options) unit = determine_unit(units, exponent) - format.gsub("%n".freeze, rounded_number).gsub("%u".freeze, unit).strip + format.gsub("%n", rounded_number).gsub("%u", unit).strip end private diff --git a/activesupport/lib/active_support/number_helper/number_to_human_size_converter.rb b/activesupport/lib/active_support/number_helper/number_to_human_size_converter.rb index 842f2fc8df..91262fa656 100644 --- a/activesupport/lib/active_support/number_helper/number_to_human_size_converter.rb +++ b/activesupport/lib/active_support/number_helper/number_to_human_size_converter.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require "active_support/number_helper/number_converter" + module ActiveSupport module NumberHelper class NumberToHumanSizeConverter < NumberConverter #:nodoc: @@ -22,7 +24,7 @@ module ActiveSupport human_size = number / (base**exponent) number_to_format = NumberToRoundedConverter.convert(human_size, options) end - conversion_format.gsub("%n".freeze, number_to_format).gsub("%u".freeze, unit) + conversion_format.gsub("%n", number_to_format).gsub("%u", unit) end private diff --git a/activesupport/lib/active_support/number_helper/number_to_percentage_converter.rb b/activesupport/lib/active_support/number_helper/number_to_percentage_converter.rb index 4dcdad2e2c..0c2e190f8a 100644 --- a/activesupport/lib/active_support/number_helper/number_to_percentage_converter.rb +++ b/activesupport/lib/active_support/number_helper/number_to_percentage_converter.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require "active_support/number_helper/number_converter" + module ActiveSupport module NumberHelper class NumberToPercentageConverter < NumberConverter # :nodoc: @@ -7,7 +9,7 @@ module ActiveSupport def convert rounded_number = NumberToRoundedConverter.convert(number, options) - options[:format].gsub("%n".freeze, rounded_number) + options[:format].gsub("%n", rounded_number) end end 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 96410f4995..d5e72981b4 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 @@ -1,5 +1,7 @@ # frozen_string_literal: true +require "active_support/number_helper/number_converter" + module ActiveSupport module NumberHelper class NumberToPhoneConverter < NumberConverter #:nodoc: 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 eb528a0583..6ceb9a572e 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 @@ -1,5 +1,7 @@ # frozen_string_literal: true +require "active_support/number_helper/number_converter" + module ActiveSupport module NumberHelper class NumberToRoundedConverter < NumberConverter # :nodoc: @@ -20,9 +22,9 @@ module ActiveSupport formatted_string = if BigDecimal === rounded_number && rounded_number.finite? s = rounded_number.to_s("F") - s << "0".freeze * precision - a, b = s.split(".".freeze, 2) - a << ".".freeze + s << "0" * precision + a, b = s.split(".", 2) + a << "." a << b[0, precision] else "%00.#{precision}f" % rounded_number diff --git a/activesupport/lib/active_support/parameter_filter.rb b/activesupport/lib/active_support/parameter_filter.rb new file mode 100644 index 0000000000..1389d82523 --- /dev/null +++ b/activesupport/lib/active_support/parameter_filter.rb @@ -0,0 +1,124 @@ +# frozen_string_literal: true + +require "active_support/core_ext/object/duplicable" +require "active_support/core_ext/array/extract" + +module ActiveSupport + # +ParameterFilter+ allows you to specify keys for sensitive data from + # hash-like object and replace corresponding value. Filtering only certain + # sub-keys from a hash is possible by using the dot notation: + # 'credit_card.number'. If a proc is given, each key and value of a hash and + # all sub-hashes are passed to it, where the value or the key can be replaced + # using String#replace or similar methods. + # + # ActiveSupport::ParameterFilter.new([:password]) + # => replaces the value to all keys matching /password/i with "[FILTERED]" + # + # ActiveSupport::ParameterFilter.new([:foo, "bar"]) + # => replaces the value to all keys matching /foo|bar/i with "[FILTERED]" + # + # ActiveSupport::ParameterFilter.new(["credit_card.code"]) + # => replaces { credit_card: {code: "xxxx"} } with "[FILTERED]", does not + # change { file: { code: "xxxx"} } + # + # ActiveSupport::ParameterFilter.new([-> (k, v) do + # v.reverse! if k =~ /secret/i + # end]) + # => reverses the value to all keys matching /secret/i + class ParameterFilter + FILTERED = "[FILTERED]" # :nodoc: + + # Create instance with given filters. Supported type of filters are +String+, +Regexp+, and +Proc+. + # Other types of filters are treated as +String+ using +to_s+. + # For +Proc+ filters, key, value, and optional original hash is passed to block arguments. + # + # ==== Options + # + # * <tt>:mask</tt> - A replaced object when filtered. Defaults to +"[FILTERED]"+ + def initialize(filters = [], mask: FILTERED) + @filters = filters + @mask = mask + end + + # Mask value of +params+ if key matches one of filters. + def filter(params) + compiled_filter.call(params) + end + + # Returns filtered value for given key. For +Proc+ filters, third block argument is not populated. + def filter_param(key, value) + @filters.empty? ? value : compiled_filter.value_for_key(key, value) + end + + private + + def compiled_filter + @compiled_filter ||= CompiledFilter.compile(@filters, mask: @mask) + end + + class CompiledFilter # :nodoc: + def self.compile(filters, mask:) + return lambda { |params| params.dup } if filters.empty? + + strings, regexps, blocks = [], [], [] + + filters.each do |item| + case item + when Proc + blocks << item + when Regexp + regexps << item + else + strings << Regexp.escape(item.to_s) + end + end + + deep_regexps = regexps.extract! { |r| r.to_s.include?("\\.") } + deep_strings = strings.extract! { |s| s.include?("\\.") } + + regexps << Regexp.new(strings.join("|"), true) unless strings.empty? + deep_regexps << Regexp.new(deep_strings.join("|"), true) unless deep_strings.empty? + + new regexps, deep_regexps, blocks, mask: mask + end + + attr_reader :regexps, :deep_regexps, :blocks + + def initialize(regexps, deep_regexps, blocks, mask:) + @regexps = regexps + @deep_regexps = deep_regexps.any? ? deep_regexps : nil + @blocks = blocks + @mask = mask + end + + def call(params, parents = [], original_params = params) + filtered_params = params.class.new + + params.each do |key, value| + filtered_params[key] = value_for_key(key, value, parents, original_params) + end + + filtered_params + end + + def value_for_key(key, value, parents = [], original_params = nil) + parents.push(key) if deep_regexps + if regexps.any? { |r| r.match?(key) } + value = @mask + elsif deep_regexps && (joined = parents.join(".")) && deep_regexps.any? { |r| r.match?(joined) } + value = @mask + elsif value.is_a?(Hash) + value = call(value, parents, original_params) + elsif value.is_a?(Array) + value = value.map { |v| v.is_a?(Hash) ? call(v, parents, original_params) : v } + elsif blocks.any? + key = key.dup if key.duplicable? + value = value.dup if value.duplicable? + blocks.each { |b| b.arity == 2 ? b.call(key, value) : b.call(key, value, original_params) } + end + parents.pop if deep_regexps + value + end + end + end +end diff --git a/activesupport/lib/active_support/rails.rb b/activesupport/lib/active_support/rails.rb index 5c34a0abb3..8b727a69ec 100644 --- a/activesupport/lib/active_support/rails.rb +++ b/activesupport/lib/active_support/rails.rb @@ -27,9 +27,3 @@ require "active_support/core_ext/module/delegation" # Defines ActiveSupport::Deprecation. require "active_support/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" diff --git a/activesupport/lib/active_support/railtie.rb b/activesupport/lib/active_support/railtie.rb index 3488721df9..605b50d346 100644 --- a/activesupport/lib/active_support/railtie.rb +++ b/activesupport/lib/active_support/railtie.rb @@ -10,9 +10,11 @@ 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 + config.after_initialize do + unless app.config.active_support.use_authenticated_message_encryption.nil? + ActiveSupport::MessageEncryptor.use_authenticated_message_encryption = + app.config.active_support.use_authenticated_message_encryption + end end end @@ -68,9 +70,10 @@ module ActiveSupport end initializer "active_support.set_hash_digest_class" do |app| - if app.config.active_support.respond_to?(:use_hash_digest_class) && app.config.active_support.use_hash_digest_class - ActiveSupport::Digest.hash_digest_class = - app.config.active_support.use_hash_digest_class + config.after_initialize do + if app.config.active_support.use_sha1_digests + ActiveSupport::Digest.hash_digest_class = ::Digest::SHA1 + end end end end diff --git a/activesupport/lib/active_support/reloader.rb b/activesupport/lib/active_support/reloader.rb index b26d9c3665..2f81cd4f80 100644 --- a/activesupport/lib/active_support/reloader.rb +++ b/activesupport/lib/active_support/reloader.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require "active_support/execution_wrapper" +require "active_support/executor" module ActiveSupport #-- @@ -49,11 +50,9 @@ module ActiveSupport def self.reload! executor.wrap do new.tap do |instance| - begin - instance.run! - ensure - instance.complete! - end + instance.run! + ensure + instance.complete! end end prepare! diff --git a/activesupport/lib/active_support/subscriber.rb b/activesupport/lib/active_support/subscriber.rb index d6dd5474d0..f3e902f9dd 100644 --- a/activesupport/lib/active_support/subscriber.rb +++ b/activesupport/lib/active_support/subscriber.rb @@ -54,25 +54,20 @@ module ActiveSupport @@subscribers ||= [] end - # TODO Change this to private once we've dropped Ruby 2.2 support. - # Workaround for Ruby 2.2 "private attribute?" warning. - protected - - attr_reader :subscriber, :notifier, :namespace - private + attr_reader :subscriber, :notifier, :namespace - def add_event_subscriber(event) # :doc: - return if %w{ start finish }.include?(event.to_s) + def add_event_subscriber(event) # :doc: + return if %w{ start finish }.include?(event.to_s) - pattern = "#{event}.#{namespace}" + pattern = "#{event}.#{namespace}" - # Don't add multiple subscribers (eg. if methods are redefined). - return if subscriber.patterns.include?(pattern) + # Don't add multiple subscribers (eg. if methods are redefined). + return if subscriber.patterns.include?(pattern) - subscriber.patterns << pattern - notifier.subscribe(pattern, subscriber) - end + subscriber.patterns << pattern + notifier.subscribe(pattern, subscriber) + end end attr_reader :patterns # :nodoc: @@ -84,25 +79,24 @@ module ActiveSupport end def start(name, id, payload) - e = ActiveSupport::Notifications::Event.new(name, Time.now, nil, id, payload) + event = ActiveSupport::Notifications::Event.new(name, nil, nil, id, payload) + event.start! parent = event_stack.last - parent << e if parent + parent << event if parent - event_stack.push e + event_stack.push event end def finish(name, id, payload) - finished = Time.now - event = event_stack.pop - event.end = finished + event = event_stack.pop + event.finish! event.payload.merge!(payload) - method = name.split(".".freeze).first + method = name.split(".").first send(method, event) end private - def event_stack SubscriberQueueRegistry.instance.get_queue(@queue_key) end diff --git a/activesupport/lib/active_support/tagged_logging.rb b/activesupport/lib/active_support/tagged_logging.rb index 8561cba9f1..d8a86d997e 100644 --- a/activesupport/lib/active_support/tagged_logging.rb +++ b/activesupport/lib/active_support/tagged_logging.rb @@ -46,21 +46,30 @@ module ActiveSupport def current_tags # We use our object ID here to avoid conflicting with other instances - thread_key = @thread_key ||= "activesupport_tagged_logging_tags:#{object_id}".freeze + thread_key = @thread_key ||= "activesupport_tagged_logging_tags:#{object_id}" Thread.current[thread_key] ||= [] end def tags_text tags = current_tags - if tags.any? + if tags.one? + "[#{tags[0]}] " + elsif tags.any? tags.collect { |tag| "[#{tag}] " }.join end end end def self.new(logger) - # Ensure we set a default formatter so we aren't extending nil! - logger.formatter ||= ActiveSupport::Logger::SimpleFormatter.new + logger = logger.dup + + if logger.formatter + logger.formatter = logger.formatter.dup + else + # Ensure we set a default formatter so we aren't extending nil! + logger.formatter = ActiveSupport::Logger::SimpleFormatter.new + end + logger.formatter.extend Formatter logger.extend(self) end diff --git a/activesupport/lib/active_support/test_case.rb b/activesupport/lib/active_support/test_case.rb index d1f7e6ea09..7be4108ed7 100644 --- a/activesupport/lib/active_support/test_case.rb +++ b/activesupport/lib/active_support/test_case.rb @@ -11,6 +11,8 @@ 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/testing/parallelization" +require "concurrent/utility/processor_counter" module ActiveSupport class TestCase < ::Minitest::Test @@ -39,12 +41,101 @@ module ActiveSupport def test_order ActiveSupport.test_order ||= :random end + + # Parallelizes the test suite. + # + # Takes a +workers+ argument that controls how many times the process + # is forked. For each process a new database will be created suffixed + # with the worker number. + # + # test-database-0 + # test-database-1 + # + # If <tt>ENV["PARALLEL_WORKERS"]</tt> is set the workers argument will be ignored + # and the environment variable will be used instead. This is useful for CI + # environments, or other environments where you may need more workers than + # you do for local testing. + # + # If the number of workers is set to +1+ or fewer, the tests will not be + # parallelized. + # + # If +workers+ is set to +:number_of_processors+, the number of workers will be + # set to the actual core count on the machine you are on. + # + # The default parallelization method is to fork processes. If you'd like to + # use threads instead you can pass <tt>with: :threads</tt> to the +parallelize+ + # method. Note the threaded parallelization does not create multiple + # database and will not work with system tests at this time. + # + # parallelize(workers: :number_of_processors, with: :threads) + # + # The threaded parallelization uses minitest's parallel executor directly. + # The processes parallelization uses a Ruby DRb server. + def parallelize(workers: :number_of_processors, with: :processes) + workers = Concurrent.physical_processor_count if workers == :number_of_processors + workers = ENV["PARALLEL_WORKERS"].to_i if ENV["PARALLEL_WORKERS"] + + return if workers <= 1 + + executor = case with + when :processes + Testing::Parallelization.new(workers) + when :threads + Minitest::Parallel::Executor.new(workers) + else + raise ArgumentError, "#{with} is not a supported parallelization executor." + end + + self.lock_threads = false if defined?(self.lock_threads) && with == :threads + + Minitest.parallel_executor = executor + + parallelize_me! + end + + # Set up hook for parallel testing. This can be used if you have multiple + # databases or any behavior that needs to be run after the process is forked + # but before the tests run. + # + # Note: this feature is not available with the threaded parallelization. + # + # In your +test_helper.rb+ add the following: + # + # class ActiveSupport::TestCase + # parallelize_setup do + # # create databases + # end + # end + def parallelize_setup(&block) + ActiveSupport::Testing::Parallelization.after_fork_hook do |worker| + yield worker + end + end + + # Clean up hook for parallel testing. This can be used to drop databases + # if your app uses multiple write/read databases or other clean up before + # the tests finish. This runs before the forked process is closed. + # + # Note: this feature is not available with the threaded parallelization. + # + # In your +test_helper.rb+ add the following: + # + # class ActiveSupport::TestCase + # parallelize_teardown do + # # drop databases + # end + # end + def parallelize_teardown(&block) + ActiveSupport::Testing::Parallelization.run_cleanup_hook do |worker| + yield worker + end + end end alias_method :method_name, :name include ActiveSupport::Testing::TaggedLogging - include ActiveSupport::Testing::SetupAndTeardown + prepend ActiveSupport::Testing::SetupAndTeardown include ActiveSupport::Testing::Assertions include ActiveSupport::Testing::Deprecation include ActiveSupport::Testing::TimeHelpers diff --git a/activesupport/lib/active_support/testing/assertions.rb b/activesupport/lib/active_support/testing/assertions.rb index b24aa36ede..b27ac7ce99 100644 --- a/activesupport/lib/active_support/testing/assertions.rb +++ b/activesupport/lib/active_support/testing/assertions.rb @@ -58,6 +58,12 @@ module ActiveSupport # post :create, params: { article: {...} } # end # + # A hash of expressions/numeric differences can also be passed in and evaluated. + # + # assert_difference ->{ Article.count } => 1, ->{ Notification.count } => 2 do + # post :create, params: { article: {...} } + # end + # # A lambda or a list of lambdas can be passed in and evaluated: # # assert_difference ->{ Article.count }, 2 do @@ -73,20 +79,28 @@ module ActiveSupport # assert_difference 'Article.count', -1, 'An Article should be destroyed' do # post :delete, params: { id: ... } # end - def assert_difference(expression, difference = 1, message = nil, &block) - expressions = Array(expression) - - exps = expressions.map { |e| + def assert_difference(expression, *args, &block) + expressions = + if expression.is_a?(Hash) + message = args[0] + expression + else + difference = args[0] || 1 + message = args[1] + Hash[Array(expression).map { |e| [e, difference] }] + end + + exps = expressions.keys.map { |e| e.respond_to?(:call) ? e : lambda { eval(e, block.binding) } } before = exps.map(&:call) retval = yield - expressions.zip(exps).each_with_index do |(code, e), i| - error = "#{code.inspect} didn't change by #{difference}" + expressions.zip(exps, before) do |(code, diff), exp, before_value| + error = "#{code.inspect} didn't change by #{diff}" error = "#{message}.\n#{error}" if message - assert_equal(before[i] + difference, e.call, error) + assert_equal(before_value + diff, exp.call, error) end retval @@ -99,11 +113,23 @@ module ActiveSupport # post :create, params: { article: invalid_attributes } # end # + # A lambda can be passed in and evaluated. + # + # assert_no_difference -> { Article.count } do + # post :create, params: { article: invalid_attributes } + # end + # # An error message can be specified. # # assert_no_difference 'Article.count', 'An Article should not be created' do # post :create, params: { article: invalid_attributes } # end + # + # An array of expressions can also be passed in and evaluated. + # + # assert_no_difference [ 'Article.count', -> { Post.count } ] do + # post :create, params: { article: invalid_attributes } + # end def assert_no_difference(expression, message = nil, &block) assert_difference expression, 0, message, &block end @@ -156,12 +182,15 @@ module ActiveSupport after = exp.call - if to == UNTRACKED - error = "#{expression.inspect} didn't change" - error = "#{message}.\n#{error}" if message - assert before != after, error - else - error = "#{expression.inspect} didn't change to #{to}" + error = "#{expression.inspect} didn't change" + error = "#{error}. It was already #{to}" if before == to + error = "#{message}.\n#{error}" if message + assert before != after, error + + unless to == UNTRACKED + error = "#{expression.inspect} didn't change to as expected\n" + error = "#{error}Expected: #{to.inspect}\n" + error = "#{error} Actual: #{after.inspect}" error = "#{message}.\n#{error}" if message assert to === after, error end diff --git a/activesupport/lib/active_support/testing/deprecation.rb b/activesupport/lib/active_support/testing/deprecation.rb index f655435729..18d63d2780 100644 --- a/activesupport/lib/active_support/testing/deprecation.rb +++ b/activesupport/lib/active_support/testing/deprecation.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true require "active_support/deprecation" -require "active_support/core_ext/regexp" module ActiveSupport module Testing diff --git a/activesupport/lib/active_support/testing/file_fixtures.rb b/activesupport/lib/active_support/testing/file_fixtures.rb index ad923d1aab..4eb7a88576 100644 --- a/activesupport/lib/active_support/testing/file_fixtures.rb +++ b/activesupport/lib/active_support/testing/file_fixtures.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require "active_support/concern" + module ActiveSupport module Testing # Adds simple access to sample files called file fixtures. diff --git a/activesupport/lib/active_support/testing/isolation.rb b/activesupport/lib/active_support/testing/isolation.rb index fa9bebb181..652a10da23 100644 --- a/activesupport/lib/active_support/testing/isolation.rb +++ b/activesupport/lib/active_support/testing/isolation.rb @@ -45,7 +45,8 @@ module ActiveSupport end } end - result = Marshal.dump(dup) + test_result = defined?(Minitest::Result) ? Minitest::Result.from(self) : dup + result = Marshal.dump(test_result) end write.puts [result].pack("m") @@ -55,7 +56,7 @@ module ActiveSupport write.close result = read.read Process.wait2(pid) - result.unpack("m")[0] + result.unpack1("m") end end @@ -69,8 +70,9 @@ module ActiveSupport if ENV["ISOLATION_TEST"] yield + test_result = defined?(Minitest::Result) ? Minitest::Result.from(self) : dup File.open(ENV["ISOLATION_OUTPUT"], "w") do |file| - file.puts [Marshal.dump(dup)].pack("m") + file.puts [Marshal.dump(test_result)].pack("m") end exit! else @@ -96,7 +98,7 @@ module ActiveSupport nil end - return tmpfile.read.unpack("m")[0] + return tmpfile.read.unpack1("m") end end end diff --git a/activesupport/lib/active_support/testing/method_call_assertions.rb b/activesupport/lib/active_support/testing/method_call_assertions.rb index c6358002ea..03c38be481 100644 --- a/activesupport/lib/active_support/testing/method_call_assertions.rb +++ b/activesupport/lib/active_support/testing/method_call_assertions.rb @@ -17,7 +17,7 @@ module ActiveSupport assert_equal times, times_called, error end - def assert_called_with(object, method_name, args = [], returns: nil) + def assert_called_with(object, method_name, args, returns: nil) mock = Minitest::Mock.new if args.all? { |arg| arg.is_a?(Array) } @@ -35,6 +35,33 @@ module ActiveSupport assert_called(object, method_name, message, times: 0, &block) end + def assert_called_on_instance_of(klass, method_name, message = nil, times: 1, returns: nil) + times_called = 0 + klass.define_method("stubbed_#{method_name}") do |*| + times_called += 1 + + returns + end + + klass.alias_method "original_#{method_name}", method_name + klass.alias_method method_name, "stubbed_#{method_name}" + + yield + + error = "Expected #{method_name} to be called #{times} times, but was called #{times_called} times" + error = "#{message}.\n#{error}" if message + + assert_equal times, times_called, error + ensure + klass.alias_method method_name, "original_#{method_name}" + klass.undef_method "original_#{method_name}" + klass.undef_method "stubbed_#{method_name}" + end + + def assert_not_called_on_instance_of(klass, method_name, message = nil, &block) + assert_called_on_instance_of(klass, method_name, message, times: 0, &block) + end + def stub_any_instance(klass, instance: klass.new) klass.stub(:new, instance) { yield instance } end diff --git a/activesupport/lib/active_support/testing/parallelization.rb b/activesupport/lib/active_support/testing/parallelization.rb new file mode 100644 index 0000000000..63440069b1 --- /dev/null +++ b/activesupport/lib/active_support/testing/parallelization.rb @@ -0,0 +1,109 @@ +# frozen_string_literal: true + +require "drb" +require "drb/unix" unless Gem.win_platform? +require "active_support/core_ext/module/attribute_accessors" + +module ActiveSupport + module Testing + class Parallelization # :nodoc: + class Server + include DRb::DRbUndumped + + def initialize + @queue = Queue.new + end + + def record(reporter, result) + raise DRb::DRbConnError if result.is_a?(DRb::DRbUnknown) + + reporter.synchronize do + reporter.record(result) + end + end + + def <<(o) + o[2] = DRbObject.new(o[2]) if o + @queue << o + end + + def pop; @queue.pop; end + end + + @@after_fork_hooks = [] + + def self.after_fork_hook(&blk) + @@after_fork_hooks << blk + end + + cattr_reader :after_fork_hooks + + @@run_cleanup_hooks = [] + + def self.run_cleanup_hook(&blk) + @@run_cleanup_hooks << blk + end + + cattr_reader :run_cleanup_hooks + + def initialize(queue_size) + @queue_size = queue_size + @queue = Server.new + @pool = [] + + @url = DRb.start_service("drbunix:", @queue).uri + end + + def after_fork(worker) + self.class.after_fork_hooks.each do |cb| + cb.call(worker) + end + end + + def run_cleanup(worker) + self.class.run_cleanup_hooks.each do |cb| + cb.call(worker) + end + end + + def start + @pool = @queue_size.times.map do |worker| + fork do + DRb.stop_service + + after_fork(worker) + + queue = DRbObject.new_with_uri(@url) + + while job = queue.pop + klass = job[0] + method = job[1] + reporter = job[2] + result = Minitest.run_one_method(klass, method) + + begin + queue.record(reporter, result) + rescue DRb::DRbConnError + result.failures.each do |failure| + failure.exception = DRb::DRbRemoteError.new(failure.exception) + end + queue.record(reporter, result) + end + end + ensure + run_cleanup(worker) + end + end + end + + def <<(work) + @queue << work + end + + def shutdown + @queue_size.times { @queue << nil } + @pool.each { |pid| Process.waitpid pid } + end + end + end +end diff --git a/activesupport/lib/active_support/testing/setup_and_teardown.rb b/activesupport/lib/active_support/testing/setup_and_teardown.rb index 1dbf3c5da0..35321cd157 100644 --- a/activesupport/lib/active_support/testing/setup_and_teardown.rb +++ b/activesupport/lib/active_support/testing/setup_and_teardown.rb @@ -1,6 +1,5 @@ # frozen_string_literal: true -require "active_support/concern" require "active_support/callbacks" module ActiveSupport @@ -19,11 +18,10 @@ module ActiveSupport # end # end module SetupAndTeardown - extend ActiveSupport::Concern - - included do - include ActiveSupport::Callbacks - define_callbacks :setup, :teardown + def self.prepended(klass) + klass.include ActiveSupport::Callbacks + klass.define_callbacks :setup, :teardown + klass.extend ClassMethods end module ClassMethods @@ -44,7 +42,12 @@ module ActiveSupport end def after_teardown # :nodoc: - run_callbacks :teardown + begin + run_callbacks :teardown + rescue => e + self.failures << Minitest::UnexpectedError.new(e) + end + super end end diff --git a/activesupport/lib/active_support/testing/stream.rb b/activesupport/lib/active_support/testing/stream.rb index d070a1793d..127cfe1e12 100644 --- a/activesupport/lib/active_support/testing/stream.rb +++ b/activesupport/lib/active_support/testing/stream.rb @@ -33,7 +33,7 @@ module ActiveSupport yield stream_io.rewind - return captured_stream.read + captured_stream.read ensure captured_stream.close captured_stream.unlink diff --git a/activesupport/lib/active_support/testing/time_helpers.rb b/activesupport/lib/active_support/testing/time_helpers.rb index 998a51a34c..5a3fa9346c 100644 --- a/activesupport/lib/active_support/testing/time_helpers.rb +++ b/activesupport/lib/active_support/testing/time_helpers.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true require "active_support/core_ext/module/redefine_method" -require "active_support/core_ext/string/strip" # for strip_heredoc require "active_support/core_ext/time/calculations" require "concurrent/map" @@ -23,7 +22,7 @@ module ActiveSupport @stubs[object.object_id][method_name] = Stub.new(object, method_name, new_name) - object.singleton_class.send :alias_method, new_name, method_name + object.singleton_class.alias_method new_name, method_name object.define_singleton_method(method_name, &block) end @@ -44,9 +43,9 @@ module ActiveSupport def unstub_object(stub) singleton_class = stub.object.singleton_class - singleton_class.send :silence_redefinition_of_method, stub.method_name - singleton_class.send :alias_method, stub.method_name, stub.original_method - singleton_class.send :undef_method, stub.original_method + singleton_class.silence_redefinition_of_method stub.method_name + singleton_class.alias_method stub.method_name, stub.original_method + singleton_class.undef_method stub.original_method end end @@ -112,7 +111,7 @@ module ActiveSupport # Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00 def travel_to(date_or_time) if block_given? && simple_stubs.stubbing(Time, :now) - travel_to_nested_block_call = <<-MSG.strip_heredoc + travel_to_nested_block_call = <<~MSG Calling `travel_to` with a block, when we have previously already made a call to `travel_to`, can lead to confusing time stubbing. @@ -159,7 +158,7 @@ module ActiveSupport end # Returns the current time back to its original state, by removing the stubs added by - # +travel+ and +travel_to+. + # +travel+, +travel_to+, and +freeze_time+. # # Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00 # travel_to Time.zone.local(2004, 11, 24, 01, 04, 44) @@ -169,6 +168,7 @@ module ActiveSupport def travel_back simple_stubs.unstub_all! end + alias_method :unfreeze_time, :travel_back # Calls +travel_to+ with +Time.now+. # diff --git a/activesupport/lib/active_support/time_with_zone.rb b/activesupport/lib/active_support/time_with_zone.rb index 20650ce714..3be5f6f7b5 100644 --- a/activesupport/lib/active_support/time_with_zone.rb +++ b/activesupport/lib/active_support/time_with_zone.rb @@ -43,8 +43,8 @@ module ActiveSupport "Time" end - PRECISIONS = Hash.new { |h, n| h[n] = "%FT%T.%#{n}N".freeze } - PRECISIONS[0] = "%FT%T".freeze + PRECISIONS = Hash.new { |h, n| h[n] = "%FT%T.%#{n}N" } + PRECISIONS[0] = "%FT%T" include Comparable, DateAndTime::Compatibility attr_reader :time_zone @@ -147,7 +147,7 @@ module ActiveSupport # # Time.zone.now.xmlschema # => "2014-12-04T11:02:37-05:00" def xmlschema(fraction_digits = 0) - "#{time.strftime(PRECISIONS[fraction_digits.to_i])}#{formatted_offset(true, 'Z'.freeze)}" + "#{time.strftime(PRECISIONS[fraction_digits.to_i])}#{formatted_offset(true, 'Z')}" end alias_method :iso8601, :xmlschema alias_method :rfc3339, :xmlschema @@ -225,6 +225,8 @@ module ActiveSupport def <=>(other) utc <=> other end + alias_method :before?, :< + alias_method :after?, :> # Returns true if the current object's time is within the specified # +min+ and +max+ time. @@ -284,8 +286,10 @@ module ActiveSupport alias_method :since, :+ alias_method :in, :+ - # Returns a new TimeWithZone object that represents the difference between - # the current object's time and the +other+ time. + # Subtracts an interval of time and returns a new TimeWithZone object unless + # the other value `acts_like?` time. Then it will return a Float of the difference + # between the two times that represents the difference between the current + # object's time and the +other+ time. # # Time.zone = 'Eastern Time (US & Canada)' # => 'Eastern Time (US & Canada)' # now = Time.zone.now # => Mon, 03 Nov 2014 00:26:28 EST -05:00 @@ -300,6 +304,12 @@ module ActiveSupport # # now - 24.hours # => Sun, 02 Nov 2014 01:26:28 EDT -04:00 # now - 1.day # => Sun, 02 Nov 2014 00:26:28 EDT -04:00 + # + # If both the TimeWithZone object and the other value act like Time, a Float + # will be returned. + # + # Time.zone.now - 1.day.ago # => 86399.999967 + # def -(other) if other.acts_like?(:time) to_time - other.to_time diff --git a/activesupport/lib/active_support/values/time_zone.rb b/activesupport/lib/active_support/values/time_zone.rb index 4d81ac939e..d9e033e23b 100644 --- a/activesupport/lib/active_support/values/time_zone.rb +++ b/activesupport/lib/active_support/values/time_zone.rb @@ -2,7 +2,6 @@ require "tzinfo" require "concurrent/map" -require "active_support/core_ext/object/blank" module ActiveSupport # The TimeZone class serves as a wrapper around TZInfo::Timezone instances. @@ -183,8 +182,9 @@ module ActiveSupport "Samoa" => "Pacific/Apia" } - UTC_OFFSET_WITH_COLON = "%s%02d:%02d" - UTC_OFFSET_WITHOUT_COLON = UTC_OFFSET_WITH_COLON.tr(":", "") + UTC_OFFSET_WITH_COLON = "%s%02d:%02d" # :nodoc: + UTC_OFFSET_WITHOUT_COLON = UTC_OFFSET_WITH_COLON.tr(":", "") # :nodoc: + private_constant :UTC_OFFSET_WITH_COLON, :UTC_OFFSET_WITHOUT_COLON @lazy_zones_map = Concurrent::Map.new @country_zones = Concurrent::Map.new @@ -238,7 +238,7 @@ module ActiveSupport when Numeric, ActiveSupport::Duration arg *= 3600 if arg.abs <= 13 all.find { |z| z.utc_offset == arg.to_i } - else + else raise ArgumentError, "invalid argument to TimeZone[]: #{arg.inspect}" end end @@ -266,9 +266,12 @@ module ActiveSupport private def load_country_zones(code) country = TZInfo::Country.get(code) - country.zone_identifiers.map do |tz_id| + country.zone_identifiers.flat_map do |tz_id| if MAPPING.value?(tz_id) - self[MAPPING.key(tz_id)] + MAPPING.inject([]) do |memo, (key, value)| + memo << self[key] if value == tz_id + memo + end else create(tz_id, nil, TZInfo::Timezone.new(tz_id)) end @@ -277,7 +280,8 @@ module ActiveSupport def zones_map @zones_map ||= MAPPING.each_with_object({}) do |(name, _), zones| - zones[name] = self[name] + timezone = self[name] + zones[name] = timezone if timezone end end end @@ -351,8 +355,13 @@ module ActiveSupport # Time.zone = 'Hawaii' # => "Hawaii" # Time.utc(2000).to_f # => 946684800.0 # Time.zone.at(946684800.0) # => Fri, 31 Dec 1999 14:00:00 HST -10:00 - def at(secs) - Time.at(secs).utc.in_time_zone(self) + # + # A second argument can be supplied to specify sub-second precision. + # + # Time.zone = 'Hawaii' # => "Hawaii" + # Time.at(946684800, 123456.789).nsec # => 123456789 + def at(*args) + Time.at(*args).utc.in_time_zone(self) end # Method for creating new ActiveSupport::TimeWithZone instance in time zone diff --git a/activesupport/lib/active_support/values/unicode_tables.dat b/activesupport/lib/active_support/values/unicode_tables.dat Binary files differdeleted file mode 100644 index f7d9c48bbe..0000000000 --- a/activesupport/lib/active_support/values/unicode_tables.dat +++ /dev/null diff --git a/activesupport/lib/active_support/xml_mini.rb b/activesupport/lib/active_support/xml_mini.rb index ee2048cb54..075cd4ed8b 100644 --- a/activesupport/lib/active_support/xml_mini.rb +++ b/activesupport/lib/active_support/xml_mini.rb @@ -3,6 +3,7 @@ require "time" require "base64" require "bigdecimal" +require "bigdecimal/util" require "active_support/core_ext/module/delegation" require "active_support/core_ext/string/inflections" require "active_support/core_ext/date_time/calculations" @@ -48,10 +49,6 @@ module ActiveSupport "Array" => "array", "Hash" => "hash" } - - # No need to map these on Ruby 2.4+ - TYPE_NAMES["Fixnum"] = "integer" unless 0.class == Integer - TYPE_NAMES["Bignum"] = "integer" unless 0.class == Integer end FORMATTING = { @@ -72,18 +69,14 @@ module ActiveSupport "float" => Proc.new { |float| float.to_f }, "decimal" => Proc.new do |number| if String === number - begin - BigDecimal(number) - rescue ArgumentError - BigDecimal("0") - end + number.to_d else BigDecimal(number) end end, "boolean" => Proc.new { |boolean| %w(1 true).include?(boolean.to_s.strip) }, "string" => Proc.new { |string| string.to_s }, - "yaml" => Proc.new { |yaml| YAML::load(yaml) rescue yaml }, + "yaml" => Proc.new { |yaml| YAML.load(yaml) rescue yaml }, "base64Binary" => Proc.new { |bin| ::Base64.decode64(bin) }, "binary" => Proc.new { |bin, entity| _parse_binary(bin, entity) }, "file" => Proc.new { |file, entity| _parse_file(file, entity) } diff --git a/activesupport/lib/active_support/xml_mini/jdom.rb b/activesupport/lib/active_support/xml_mini/jdom.rb index 7f94a64016..32fe6ade28 100644 --- a/activesupport/lib/active_support/xml_mini/jdom.rb +++ b/activesupport/lib/active_support/xml_mini/jdom.rb @@ -18,7 +18,7 @@ module ActiveSupport module XmlMini_JDOM #:nodoc: extend self - CONTENT_KEY = "__content__".freeze + CONTENT_KEY = "__content__" NODE_TYPE_NAMES = %w{ATTRIBUTE_NODE CDATA_SECTION_NODE COMMENT_NODE DOCUMENT_FRAGMENT_NODE DOCUMENT_NODE DOCUMENT_TYPE_NODE ELEMENT_NODE ENTITY_NODE ENTITY_REFERENCE_NODE NOTATION_NODE @@ -169,7 +169,7 @@ module ActiveSupport # element:: # XML element to be checked. def empty_content?(element) - text = "".dup + text = +"" child_nodes = element.child_nodes (0...child_nodes.length).each do |i| item = child_nodes.item(i) diff --git a/activesupport/lib/active_support/xml_mini/libxml.rb b/activesupport/lib/active_support/xml_mini/libxml.rb index 0b000fea60..c2e999ef6c 100644 --- a/activesupport/lib/active_support/xml_mini/libxml.rb +++ b/activesupport/lib/active_support/xml_mini/libxml.rb @@ -34,7 +34,7 @@ module LibXML #:nodoc: end module Node #:nodoc: - CONTENT_ROOT = "__content__".freeze + CONTENT_ROOT = "__content__" # Convert XML document to hash. # @@ -55,7 +55,7 @@ module LibXML #:nodoc: if c.element? c.to_hash(node_hash) elsif c.text? || c.cdata? - node_hash[CONTENT_ROOT] ||= "".dup + node_hash[CONTENT_ROOT] ||= +"" 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 dcf16e6084..ac8acdfc3c 100644 --- a/activesupport/lib/active_support/xml_mini/libxmlsax.rb +++ b/activesupport/lib/active_support/xml_mini/libxmlsax.rb @@ -13,8 +13,8 @@ module ActiveSupport class HashBuilder include LibXML::XML::SaxParser::Callbacks - CONTENT_KEY = "__content__".freeze - HASH_SIZE_KEY = "__hash_size__".freeze + CONTENT_KEY = "__content__" + HASH_SIZE_KEY = "__hash_size__" attr_reader :hash @@ -23,7 +23,7 @@ module ActiveSupport end def on_start_document - @hash = { CONTENT_KEY => "".dup } + @hash = { CONTENT_KEY => +"" } @hash_stack = [@hash] end @@ -33,7 +33,7 @@ module ActiveSupport end def on_start_element(name, attrs = {}) - new_hash = { CONTENT_KEY => "".dup }.merge!(attrs) + new_hash = { CONTENT_KEY => +"" }.merge!(attrs) new_hash[HASH_SIZE_KEY] = new_hash.size + 1 case current_hash[name] diff --git a/activesupport/lib/active_support/xml_mini/nokogiri.rb b/activesupport/lib/active_support/xml_mini/nokogiri.rb index 5ee6fc8159..f76513f48b 100644 --- a/activesupport/lib/active_support/xml_mini/nokogiri.rb +++ b/activesupport/lib/active_support/xml_mini/nokogiri.rb @@ -38,7 +38,7 @@ module ActiveSupport end module Node #:nodoc: - CONTENT_ROOT = "__content__".freeze + CONTENT_ROOT = "__content__" # Convert XML document to hash. # @@ -59,7 +59,7 @@ module ActiveSupport if c.element? c.to_hash(node_hash) elsif c.text? || c.cdata? - node_hash[CONTENT_ROOT] ||= "".dup + node_hash[CONTENT_ROOT] ||= +"" 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 b01ed00a14..55cd72e093 100644 --- a/activesupport/lib/active_support/xml_mini/nokogirisax.rb +++ b/activesupport/lib/active_support/xml_mini/nokogirisax.rb @@ -16,8 +16,8 @@ module ActiveSupport # Class that will build the hash while the XML document # is being parsed using SAX events. class HashBuilder < Nokogiri::XML::SAX::Document - CONTENT_KEY = "__content__".freeze - HASH_SIZE_KEY = "__hash_size__".freeze + CONTENT_KEY = "__content__" + HASH_SIZE_KEY = "__hash_size__" attr_reader :hash @@ -39,7 +39,7 @@ module ActiveSupport end def start_element(name, attrs = []) - new_hash = { CONTENT_KEY => "".dup }.merge!(Hash[attrs]) + new_hash = { CONTENT_KEY => +"" }.merge!(Hash[attrs]) new_hash[HASH_SIZE_KEY] = new_hash.size + 1 case current_hash[name] diff --git a/activesupport/lib/active_support/xml_mini/rexml.rb b/activesupport/lib/active_support/xml_mini/rexml.rb index 32458d5b0d..8d6e3af066 100644 --- a/activesupport/lib/active_support/xml_mini/rexml.rb +++ b/activesupport/lib/active_support/xml_mini/rexml.rb @@ -8,7 +8,7 @@ module ActiveSupport module XmlMini_REXML #:nodoc: extend self - CONTENT_KEY = "__content__".freeze + CONTENT_KEY = "__content__" # Parse an XML Document string or IO into a simple hash. # @@ -76,7 +76,7 @@ module ActiveSupport hash else # must use value to prevent double-escaping - texts = "".dup + texts = +"" element.texts.each { |t| texts << t.value } merge!(hash, CONTENT_KEY, texts) end diff --git a/activesupport/test/abstract_unit.rb b/activesupport/test/abstract_unit.rb index 3ac11e0fe0..62356e4d46 100644 --- a/activesupport/test/abstract_unit.rb +++ b/activesupport/test/abstract_unit.rb @@ -29,13 +29,14 @@ I18n.enforce_available_locales = false class ActiveSupport::TestCase include ActiveSupport::Testing::MethodCallAssertions - # Skips the current run on Rubinius using Minitest::Assertions#skip - private def rubinius_skip(message = "") - skip message if RUBY_ENGINE == "rbx" - end - - # Skips the current run on JRuby using Minitest::Assertions#skip - private def jruby_skip(message = "") - skip message if defined?(JRUBY_VERSION) - end + private + # Skips the current run on Rubinius using Minitest::Assertions#skip + def rubinius_skip(message = "") + skip message if RUBY_ENGINE == "rbx" + end + + # Skips the current run on JRuby using Minitest::Assertions#skip + def jruby_skip(message = "") + skip message if defined?(JRUBY_VERSION) + end end diff --git a/activesupport/test/array_inquirer_test.rb b/activesupport/test/array_inquirer_test.rb index d5419b862d..9a260edd8a 100644 --- a/activesupport/test/array_inquirer_test.rb +++ b/activesupport/test/array_inquirer_test.rb @@ -9,9 +9,9 @@ class ArrayInquirerTest < ActiveSupport::TestCase end def test_individual - assert @array_inquirer.mobile? - assert @array_inquirer.tablet? - assert_not @array_inquirer.desktop? + assert_predicate @array_inquirer, :mobile? + assert_predicate @array_inquirer, :tablet? + assert_not_predicate @array_inquirer, :desktop? end def test_any diff --git a/activesupport/test/autoloading_fixtures/module_folder/nested_with_require.rb b/activesupport/test/autoloading_fixtures/module_folder/nested_with_require.rb new file mode 100644 index 0000000000..f9d6e675d7 --- /dev/null +++ b/activesupport/test/autoloading_fixtures/module_folder/nested_with_require.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +require "dependencies/module_folder/lib_class" + +module ModuleFolder + class NestedWithRequire + end +end diff --git a/activesupport/test/autoloading_fixtures/nested_with_require_parent.rb b/activesupport/test/autoloading_fixtures/nested_with_require_parent.rb new file mode 100644 index 0000000000..e8fb321077 --- /dev/null +++ b/activesupport/test/autoloading_fixtures/nested_with_require_parent.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class NestedWithRequireParent + ModuleFolder::NestedWithRequire +end diff --git a/activesupport/test/autoloading_fixtures/raises_load_error.rb b/activesupport/test/autoloading_fixtures/raises_load_error.rb new file mode 100644 index 0000000000..f97be29b71 --- /dev/null +++ b/activesupport/test/autoloading_fixtures/raises_load_error.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +# raises a load error typical of the dynamic code that manually raises load errors +raise LoadError, "required gem not present kind of error" diff --git a/activesupport/test/benchmarkable_test.rb b/activesupport/test/benchmarkable_test.rb index 424da0a52c..59a71d99be 100644 --- a/activesupport/test/benchmarkable_test.rb +++ b/activesupport/test/benchmarkable_test.rb @@ -26,7 +26,7 @@ class BenchmarkableTest < ActiveSupport::TestCase def test_without_block assert_raise(LocalJumpError) { benchmark } - assert buffer.empty? + assert_empty buffer end def test_defaults @@ -59,13 +59,13 @@ class BenchmarkableTest < ActiveSupport::TestCase def test_within_level logger.level = ActiveSupport::Logger::DEBUG - benchmark("included_debug_run", level: :debug) {} + benchmark("included_debug_run", level: :debug) { } assert_last_logged "included_debug_run" end def test_outside_level logger.level = ActiveSupport::Logger::ERROR - benchmark("skipped_debug_run", level: :debug) {} + benchmark("skipped_debug_run", level: :debug) { } assert_no_match(/skipped_debug_run/, buffer.last) ensure logger.level = ActiveSupport::Logger::DEBUG diff --git a/activesupport/test/broadcast_logger_test.rb b/activesupport/test/broadcast_logger_test.rb index 181113e70a..7dfa8a62bd 100644 --- a/activesupport/test/broadcast_logger_test.rb +++ b/activesupport/test/broadcast_logger_test.rb @@ -114,7 +114,17 @@ module ActiveSupport assert_equal [[::Logger::FATAL, "seen", nil]], log2.adds end + test "Including top constant LoggerSilence is deprecated" do + assert_deprecated("Please use `ActiveSupport::LoggerSilence`") do + Class.new(CustomLogger) do + include ::LoggerSilence + end + end + end + class CustomLogger + include ActiveSupport::LoggerSilence + attr_reader :adds, :closed, :chevrons attr_accessor :level, :progname, :formatter, :local_level @@ -166,7 +176,6 @@ module ActiveSupport end class FakeLogger < CustomLogger - include LoggerSilence end end end diff --git a/activesupport/test/cache/behaviors.rb b/activesupport/test/cache/behaviors.rb index cb08a10bba..2d39976be3 100644 --- a/activesupport/test/cache/behaviors.rb +++ b/activesupport/test/cache/behaviors.rb @@ -3,7 +3,10 @@ require_relative "behaviors/autoloading_cache_behavior" require_relative "behaviors/cache_delete_matched_behavior" require_relative "behaviors/cache_increment_decrement_behavior" +require_relative "behaviors/cache_instrumentation_behavior" require_relative "behaviors/cache_store_behavior" require_relative "behaviors/cache_store_version_behavior" +require_relative "behaviors/connection_pool_behavior" require_relative "behaviors/encoded_key_cache_behavior" +require_relative "behaviors/failure_safety_behavior" require_relative "behaviors/local_cache_behavior" diff --git a/activesupport/test/cache/behaviors/cache_delete_matched_behavior.rb b/activesupport/test/cache/behaviors/cache_delete_matched_behavior.rb index 6f59ce48d2..ed8eba8fc2 100644 --- a/activesupport/test/cache/behaviors/cache_delete_matched_behavior.rb +++ b/activesupport/test/cache/behaviors/cache_delete_matched_behavior.rb @@ -7,9 +7,9 @@ module CacheDeleteMatchedBehavior @cache.write("foo/bar", "baz") @cache.write("fu/baz", "bar") @cache.delete_matched(/oo/) - assert !@cache.exist?("foo") + assert_not @cache.exist?("foo") assert @cache.exist?("fu") - assert !@cache.exist?("foo/bar") + assert_not @cache.exist?("foo/bar") assert @cache.exist?("fu/baz") end end diff --git a/activesupport/test/cache/cache_store_write_multi_test.rb b/activesupport/test/cache/behaviors/cache_instrumentation_behavior.rb index 5b6fd678c5..a4abdd37b9 100644 --- a/activesupport/test/cache/cache_store_write_multi_test.rb +++ b/activesupport/test/cache/behaviors/cache_instrumentation_behavior.rb @@ -1,28 +1,15 @@ # frozen_string_literal: true -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 +module CacheInstrumentationBehavior + def test_fetch_multi_uses_write_multi_entries_store_provider_interface + assert_called(@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 + def test_write_multi_instrumentation writes = { "a" => "aa", "b" => "bb" } events = with_instrumentation "write_multi" do @@ -34,16 +21,27 @@ class CacheStoreWriteMultiInstrumentationTest < ActiveSupport::TestCase 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" + def test_instrumentation_with_fetch_multi_as_super_operation + @cache.write("b", "bb") - events = with_instrumentation "write_multi" do + events = with_instrumentation "read_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] + assert_equal %w[ cache_read_multi.active_support ], events.map(&:name) + assert_equal :fetch_multi, events[0].payload[:super_operation] + assert_equal ["b"], events[0].payload[:hits] + end + + def test_read_multi_instrumentation + @cache.write("b", "bb") + + events = with_instrumentation "read_multi" do + @cache.read_multi("a", "b") { |key| key * 2 } + end + + assert_equal %w[ cache_read_multi.active_support ], events.map(&:name) + assert_equal ["b"], events[0].payload[:hits] end private diff --git a/activesupport/test/cache/behaviors/cache_store_behavior.rb b/activesupport/test/cache/behaviors/cache_store_behavior.rb index 73a9b2a71c..a696760bb2 100644 --- a/activesupport/test/cache/behaviors/cache_store_behavior.rb +++ b/activesupport/test/cache/behaviors/cache_store_behavior.rb @@ -33,7 +33,7 @@ module CacheStoreBehavior cache_miss = false assert_equal 3, @cache.fetch("foo") { |key| cache_miss = true; key.length } - assert !cache_miss + assert_not cache_miss end def test_fetch_with_forced_cache_miss @@ -52,6 +52,13 @@ module CacheStoreBehavior end end + def test_fetch_cache_miss_with_skip_nil + assert_not_called(@cache, :write) do + assert_nil @cache.fetch("foo", skip_nil: true) { nil } + assert_equal false, @cache.exist?("foo") + 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" } @@ -113,7 +120,17 @@ module CacheStoreBehavior assert_equal("fufu", @cache.read("fu")) end - def test_multi_with_objects + def test_fetch_multi_without_expires_in + @cache.write("foo", "bar") + @cache.write("fud", "biz") + + values = @cache.fetch_multi("foo", "fu", "fud", expires_in: nil) { |value| value * 2 } + + assert_equal({ "foo" => "bar", "fu" => "fufu", "fud" => "biz" }, values) + assert_equal("fufu", @cache.read("fu")) + end + + def test_fetch_multi_with_objects cache_struct = Struct.new(:cache_key, :title) foo = cache_struct.new("foo", "FOO!") bar = cache_struct.new("bar") @@ -125,35 +142,125 @@ module CacheStoreBehavior assert_equal({ foo => "FOO!", bar => "BAM!" }, values) end + def test_fetch_multi_returns_ordered_names + @cache.write("bam", "BAM") + + values = @cache.fetch_multi("foo", "bar", "bam") { |key| key.upcase } + + assert_equal(%w(foo bar bam), values.keys) + 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") + # Use strings that are guaranteed to compress well, so we can easily tell if + # the compression kicked in or not. + SMALL_STRING = "0" * 100 + LARGE_STRING = "0" * 2.kilobytes + + SMALL_OBJECT = { data: SMALL_STRING } + LARGE_OBJECT = { data: LARGE_STRING } + + def test_nil_with_default_compression_settings + assert_uncompressed(nil) end - def test_read_and_write_compressed_large_data - @cache.write("foo", "bar", compress: true, compress_threshold: 2) - assert_equal "bar", @cache.read("foo") + def test_nil_with_compress_true + assert_uncompressed(nil, compress: true) end - def test_read_and_write_compressed_nil - @cache.write("foo", nil, compress: true) - assert_nil @cache.read("foo") + def test_nil_with_compress_false + assert_uncompressed(nil, compress: false) end - def test_read_and_write_uncompressed_small_data - @cache.write("foo", "bar", compress: false) - assert_equal "bar", @cache.read("foo") + def test_nil_with_compress_low_compress_threshold + assert_uncompressed(nil, compress: true, compress_threshold: 1) end - def test_read_and_write_uncompressed_nil - @cache.write("foo", nil, compress: false) - assert_nil @cache.read("foo") + def test_small_string_with_default_compression_settings + assert_uncompressed(SMALL_STRING) + end + + def test_small_string_with_compress_true + assert_uncompressed(SMALL_STRING, compress: true) + end + + def test_small_string_with_compress_false + assert_uncompressed(SMALL_STRING, compress: false) + end + + def test_small_string_with_low_compress_threshold + assert_compressed(SMALL_STRING, compress: true, compress_threshold: 1) + end + + def test_small_object_with_default_compression_settings + assert_uncompressed(SMALL_OBJECT) + end + + def test_small_object_with_compress_true + assert_uncompressed(SMALL_OBJECT, compress: true) + end + + def test_small_object_with_compress_false + assert_uncompressed(SMALL_OBJECT, compress: false) + end + + def test_small_object_with_low_compress_threshold + assert_compressed(SMALL_OBJECT, compress: true, compress_threshold: 1) + end + + def test_large_string_with_default_compression_settings + assert_compressed(LARGE_STRING) + end + + def test_large_string_with_compress_true + assert_compressed(LARGE_STRING, compress: true) + end + + def test_large_string_with_compress_false + assert_uncompressed(LARGE_STRING, compress: false) + end + + def test_large_string_with_high_compress_threshold + assert_uncompressed(LARGE_STRING, compress: true, compress_threshold: 1.megabyte) + end + + def test_large_object_with_default_compression_settings + assert_compressed(LARGE_OBJECT) + end + + def test_large_object_with_compress_true + assert_compressed(LARGE_OBJECT, compress: true) + end + + def test_large_object_with_compress_false + assert_uncompressed(LARGE_OBJECT, compress: false) + end + + def test_large_object_with_high_compress_threshold + assert_uncompressed(LARGE_OBJECT, compress: true, compress_threshold: 1.megabyte) + end + + def test_incompressable_data + assert_uncompressed(nil, compress: true, compress_threshold: 1) + assert_uncompressed(true, compress: true, compress_threshold: 1) + assert_uncompressed(false, compress: true, compress_threshold: 1) + assert_uncompressed(0, compress: true, compress_threshold: 1) + assert_uncompressed(1.2345, compress: true, compress_threshold: 1) + assert_uncompressed("", compress: true, compress_threshold: 1) + + incompressible = nil + + # generate an incompressible string + loop do + incompressible = SecureRandom.random_bytes(1.kilobyte) + break if incompressible.bytesize < Zlib::Deflate.deflate(incompressible).bytesize + end + + assert_uncompressed(incompressible, compress: true, compress_threshold: 1) end def test_cache_key @@ -174,11 +281,72 @@ module CacheStoreBehavior assert_equal "bar", @cache.read("foo") end + def test_unversioned_cache_key + obj = Object.new + def obj.cache_key + "foo" + end + def obj.cache_key_with_version + "foo-v1" + 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 + InstanceTest = Struct.new(:name, :id) do + def cache_key + "#{name}/#{id}" + end + + def to_param + "hello" + end + end + + def test_array_with_single_instance_as_cache_key_uses_cache_key_method + test_instance_one = InstanceTest.new("test", 1) + test_instance_two = InstanceTest.new("test", 2) + + @cache.write([test_instance_one], "one") + @cache.write([test_instance_two], "two") + + assert_equal "one", @cache.read([test_instance_one]) + assert_equal "two", @cache.read([test_instance_two]) + end + + def test_array_with_multiple_instances_as_cache_key_uses_cache_key_method + test_instance_one = InstanceTest.new("test", 1) + test_instance_two = InstanceTest.new("test", 2) + test_instance_three = InstanceTest.new("test", 3) + + @cache.write([test_instance_one, test_instance_three], "one") + @cache.write([test_instance_two, test_instance_three], "two") + + assert_equal "one", @cache.read([test_instance_one, test_instance_three]) + assert_equal "two", @cache.read([test_instance_two, test_instance_three]) + end + + def test_format_of_expanded_key_for_single_instance + test_instance_one = InstanceTest.new("test", 1) + + expanded_key = @cache.send(:expanded_key, test_instance_one) + + assert_equal expanded_key, test_instance_one.cache_key + end + + def test_format_of_expanded_key_for_single_instance_in_array + test_instance_one = InstanceTest.new("test", 1) + + expanded_key = @cache.send(:expanded_key, [test_instance_one]) + + assert_equal expanded_key, test_instance_one.cache_key + end + def test_hash_as_cache_key @cache.write({ foo: 1, fu: 2 }, "bar") assert_equal "bar", @cache.read("foo=1/fu=2") @@ -204,11 +372,11 @@ module CacheStoreBehavior @cache.write("foo", "bar") assert @cache.exist?("foo") assert @cache.delete("foo") - assert !@cache.exist?("foo") + assert_not @cache.exist?("foo") end def test_original_store_objects_should_not_be_immutable - bar = "bar".dup + bar = +"bar" @cache.write("foo", bar) assert_nothing_raised { bar.gsub!(/.*/, "baz") } end @@ -297,8 +465,7 @@ module CacheStoreBehavior end def test_really_long_keys - key = "".dup - 900.times { key << "x" } + key = "x" * 2048 assert @cache.write(key, "bar") assert_equal "bar", @cache.read(key) assert_equal "bar", @cache.fetch(key) @@ -314,7 +481,7 @@ module CacheStoreBehavior @events << ActiveSupport::Notifications::Event.new(*args) end assert @cache.write(key, "1", raw: true) - assert @cache.fetch(key) {} + 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] @@ -328,7 +495,7 @@ module CacheStoreBehavior ActiveSupport::Notifications.subscribe(/^cache_(.*)\.active_support$/) do |*args| @events << ActiveSupport::Notifications::Event.new(*args) end - assert_not @cache.fetch("bad_key") {} + 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 @@ -338,4 +505,41 @@ module CacheStoreBehavior ensure ActiveSupport::Notifications.unsubscribe "cache_read.active_support" end + + private + + def assert_compressed(value, **options) + assert_compression(true, value, **options) + end + + def assert_uncompressed(value, **options) + assert_compression(false, value, **options) + end + + def assert_compression(should_compress, value, **options) + freeze_time do + @cache.write("actual", value, options) + @cache.write("uncompressed", value, options.merge(compress: false)) + end + + if value.nil? + assert_nil @cache.read("actual") + assert_nil @cache.read("uncompressed") + else + assert_equal value, @cache.read("actual") + assert_equal value, @cache.read("uncompressed") + end + + actual_entry = @cache.send(:read_entry, @cache.send(:normalize_key, "actual", {}), {}) + uncompressed_entry = @cache.send(:read_entry, @cache.send(:normalize_key, "uncompressed", {}), {}) + + actual_size = Marshal.dump(actual_entry).bytesize + uncompressed_size = Marshal.dump(uncompressed_entry).bytesize + + if should_compress + assert_operator actual_size, :<, uncompressed_size, "value should be compressed" + else + assert_equal uncompressed_size, actual_size, "value should not be compressed" + end + end end diff --git a/activesupport/test/cache/behaviors/cache_store_version_behavior.rb b/activesupport/test/cache/behaviors/cache_store_version_behavior.rb index c2e4d046af..805f061839 100644 --- a/activesupport/test/cache/behaviors/cache_store_version_behavior.rb +++ b/activesupport/test/cache/behaviors/cache_store_version_behavior.rb @@ -30,7 +30,7 @@ module CacheStoreVersionBehavior def test_exist_with_wrong_version_should_be_false @cache.write("foo", "bar", version: 1) - assert !@cache.exist?("foo", version: 2) + assert_not @cache.exist?("foo", version: 2) end def test_reading_and_writing_with_model_supporting_cache_version @@ -65,7 +65,7 @@ module CacheStoreVersionBehavior m1v2 = ModelWithKeyAndVersion.new("model/1", 2) @cache.write(m1v1, "bar") - assert @cache.exist?(m1v1) + assert @cache.exist?(m1v1) assert_not @cache.fetch(m1v2) end diff --git a/activesupport/test/cache/behaviors/connection_pool_behavior.rb b/activesupport/test/cache/behaviors/connection_pool_behavior.rb new file mode 100644 index 0000000000..5e66e0b202 --- /dev/null +++ b/activesupport/test/cache/behaviors/connection_pool_behavior.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +module ConnectionPoolBehavior + def test_connection_pool + Thread.report_on_exception, original_report_on_exception = false, Thread.report_on_exception + + threads = [] + + emulating_latency do + cache = ActiveSupport::Cache.lookup_store(store, { pool_size: 2, pool_timeout: 1 }.merge(store_options)) + cache.clear + + assert_raises Timeout::Error do + # One of the three threads will fail in 1 second because our pool size + # is only two. + 3.times do + threads << Thread.new do + cache.read("latency") + end + end + + threads.each(&:join) + end + ensure + threads.each(&:kill) + end + ensure + Thread.report_on_exception = original_report_on_exception + end + + def test_no_connection_pool + threads = [] + + emulating_latency do + cache = ActiveSupport::Cache.lookup_store(store, store_options) + cache.clear + + assert_nothing_raised do + # Default connection pool size is 5, assuming 10 will make sure that + # the connection pool isn't used at all. + 10.times do + threads << Thread.new do + cache.read("latency") + end + end + + threads.each(&:join) + end + ensure + threads.each(&:kill) + end + end + + private + def store_options; {}; end +end diff --git a/activesupport/test/cache/behaviors/encoded_key_cache_behavior.rb b/activesupport/test/cache/behaviors/encoded_key_cache_behavior.rb index da16142496..842400f4a3 100644 --- a/activesupport/test/cache/behaviors/encoded_key_cache_behavior.rb +++ b/activesupport/test/cache/behaviors/encoded_key_cache_behavior.rb @@ -6,7 +6,7 @@ module EncodedKeyCacheBehavior Encoding.list.each do |encoding| define_method "test_#{encoding.name.underscore}_encoded_values" do - key = "foo".dup.force_encoding(encoding) + key = (+"foo").force_encoding(encoding) assert @cache.write(key, "1", raw: true) assert_equal "1", @cache.read(key) assert_equal "1", @cache.fetch(key) @@ -18,7 +18,7 @@ module EncodedKeyCacheBehavior end def test_common_utf8_values - key = "\xC3\xBCmlaut".dup.force_encoding(Encoding::UTF_8) + 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) @@ -29,7 +29,7 @@ module EncodedKeyCacheBehavior end def test_retains_encoding - key = "\xC3\xBCmlaut".dup.force_encoding(Encoding::UTF_8) + key = (+"\xC3\xBCmlaut").force_encoding(Encoding::UTF_8) assert @cache.write(key, "1", raw: true) assert_equal Encoding::UTF_8, key.encoding end diff --git a/activesupport/test/cache/behaviors/failure_safety_behavior.rb b/activesupport/test/cache/behaviors/failure_safety_behavior.rb new file mode 100644 index 0000000000..43b67d81db --- /dev/null +++ b/activesupport/test/cache/behaviors/failure_safety_behavior.rb @@ -0,0 +1,91 @@ +# frozen_string_literal: true + +module FailureSafetyBehavior + def test_fetch_read_failure_returns_nil + @cache.write("foo", "bar") + + emulating_unavailability do |cache| + assert_nil cache.fetch("foo") + end + end + + def test_fetch_read_failure_does_not_attempt_to_write + end + + def test_read_failure_returns_nil + @cache.write("foo", "bar") + + emulating_unavailability do |cache| + assert_nil cache.read("foo") + end + end + + def test_read_multi_failure_returns_empty_hash + @cache.write_multi("foo" => "bar", "baz" => "quux") + + emulating_unavailability do |cache| + assert_equal Hash.new, cache.read_multi("foo", "baz") + end + end + + def test_write_failure_returns_false + emulating_unavailability do |cache| + assert_equal false, cache.write("foo", "bar") + end + end + + def test_write_multi_failure_not_raises + emulating_unavailability do |cache| + assert_nothing_raised do + cache.write_multi("foo" => "bar", "baz" => "quux") + end + end + end + + def test_fetch_multi_failure_returns_fallback_results + @cache.write_multi("foo" => "bar", "baz" => "quux") + + emulating_unavailability do |cache| + fetched = cache.fetch_multi("foo", "baz") { |k| "unavailable" } + assert_equal Hash["foo" => "unavailable", "baz" => "unavailable"], fetched + end + end + + def test_delete_failure_returns_false + @cache.write("foo", "bar") + + emulating_unavailability do |cache| + assert_equal false, cache.delete("foo") + end + end + + def test_exist_failure_returns_false + @cache.write("foo", "bar") + + emulating_unavailability do |cache| + assert_not cache.exist?("foo") + end + end + + def test_increment_failure_returns_nil + @cache.write("foo", 1, raw: true) + + emulating_unavailability do |cache| + assert_nil cache.increment("foo") + end + end + + def test_decrement_failure_returns_nil + @cache.write("foo", 1, raw: true) + + emulating_unavailability do |cache| + assert_nil cache.decrement("foo") + end + end + + def test_clear_failure_returns_nil + emulating_unavailability do |cache| + assert_nil cache.clear + end + end +end diff --git a/activesupport/test/cache/behaviors/local_cache_behavior.rb b/activesupport/test/cache/behaviors/local_cache_behavior.rb index f7302df4c8..baa38ba6ac 100644 --- a/activesupport/test/cache/behaviors/local_cache_behavior.rb +++ b/activesupport/test/cache/behaviors/local_cache_behavior.rb @@ -119,6 +119,28 @@ module LocalCacheBehavior end end + def test_local_cache_of_fetch_multi + @cache.with_local_cache do + @cache.fetch_multi("foo", "bar") { |_key| true } + @peek.delete("foo") + @peek.delete("bar") + assert_equal true, @cache.read("foo") + assert_equal true, @cache.read("bar") + end + end + + def test_local_cache_of_read_multi + @cache.with_local_cache do + @cache.write("foo", "foo", raw: true) + @cache.write("bar", "bar", raw: true) + values = @cache.read_multi("foo", "bar") + assert_equal "foo", @cache.read("foo") + assert_equal "bar", @cache.read("bar") + assert_equal "foo", values["foo"] + assert_equal "bar", values["bar"] + end + end + def test_middleware app = lambda { |env| result = @cache.write("foo", "bar") diff --git a/activesupport/test/cache/cache_entry_test.rb b/activesupport/test/cache/cache_entry_test.rb index 80ff7ad564..ec20a288e1 100644 --- a/activesupport/test/cache/cache_entry_test.rb +++ b/activesupport/test/cache/cache_entry_test.rb @@ -6,32 +6,11 @@ require "active_support/cache" class CacheEntryTest < ActiveSupport::TestCase def test_expired entry = ActiveSupport::Cache::Entry.new("value") - assert !entry.expired?, "entry not expired" + assert_not entry.expired?, "entry not expired" entry = ActiveSupport::Cache::Entry.new("value", expires_in: 60) - assert !entry.expired?, "entry not expired" + assert_not entry.expired?, "entry not expired" Time.stub(:now, Time.now + 61) do assert entry.expired?, "entry is expired" end end - - def test_compressed_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_compressed_by_default - value = "value" * 100 - entry = ActiveSupport::Cache::Entry.new(value, compress_threshold: 1) - assert_equal value, entry.value - assert(value.bytesize > entry.size, "value is compressed") - end - - def test_uncompressed_values - value = "value" * 100 - entry = ActiveSupport::Cache::Entry.new(value, compress: false) - 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 index 84e656f504..c2240d03c2 100644 --- a/activesupport/test/cache/cache_key_test.rb +++ b/activesupport/test/cache/cache_key_test.rb @@ -47,7 +47,7 @@ class CacheKeyTest < ActiveSupport::TestCase end def test_expand_cache_key_respond_to_cache_key - key = "foo".dup + key = +"foo" def key.cache_key :foo_key end @@ -55,7 +55,7 @@ class CacheKeyTest < ActiveSupport::TestCase end def test_expand_cache_key_array_with_something_that_responds_to_cache_key - key = "foo".dup + key = +"foo" def key.cache_key :foo_key end diff --git a/activesupport/test/cache/cache_store_logger_test.rb b/activesupport/test/cache/cache_store_logger_test.rb index 1af6893cc9..4648b2d361 100644 --- a/activesupport/test/cache/cache_store_logger_test.rb +++ b/activesupport/test/cache/cache_store_logger_test.rb @@ -13,7 +13,7 @@ class CacheStoreLoggerTest < ActiveSupport::TestCase def test_logging @cache.fetch("foo") { "bar" } - assert @buffer.string.present? + assert_predicate @buffer.string, :present? end def test_log_with_string_namespace @@ -31,6 +31,6 @@ class CacheStoreLoggerTest < ActiveSupport::TestCase def test_mute_logging @cache.mute { @cache.fetch("foo") { "bar" } } - assert @buffer.string.blank? + assert_predicate @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 index b52a61c500..dfdb3262f2 100644 --- a/activesupport/test/cache/cache_store_namespace_test.rb +++ b/activesupport/test/cache/cache_store_namespace_test.rb @@ -25,7 +25,7 @@ class CacheStoreNamespaceTest < ActiveSupport::TestCase cache.write("foo", "bar") cache.write("fu", "baz") cache.delete_matched(/^fo/) - assert !cache.exist?("foo") + assert_not cache.exist?("foo") assert cache.exist?("fu") end @@ -34,7 +34,7 @@ class CacheStoreNamespaceTest < ActiveSupport::TestCase cache.write("foo", "bar") cache.write("fu", "baz") cache.delete_matched(/OO/i) - assert !cache.exist?("foo") + assert_not cache.exist?("foo") assert cache.exist?("fu") end end diff --git a/activesupport/test/cache/local_cache_middleware_test.rb b/activesupport/test/cache/local_cache_middleware_test.rb index e59fae0b4c..e46fa59784 100644 --- a/activesupport/test/cache/local_cache_middleware_test.rb +++ b/activesupport/test/cache/local_cache_middleware_test.rb @@ -17,7 +17,7 @@ module ActiveSupport }) _, _, body = middleware.call({}) assert LocalCacheRegistry.cache_for(key), "should still have a cache" - body.each {} + body.each { } assert LocalCacheRegistry.cache_for(key), "should still have a cache" body.close assert_nil LocalCacheRegistry.cache_for(key) diff --git a/activesupport/test/cache/stores/file_store_test.rb b/activesupport/test/cache/stores/file_store_test.rb index 66231b0a82..0364d9ab64 100644 --- a/activesupport/test/cache/stores/file_store_test.rb +++ b/activesupport/test/cache/stores/file_store_test.rb @@ -30,6 +30,7 @@ class FileStoreTest < ActiveSupport::TestCase include LocalCacheBehavior include CacheDeleteMatchedBehavior include CacheIncrementDecrementBehavior + include CacheInstrumentationBehavior include AutoloadingCacheBehavior def test_clear @@ -67,7 +68,9 @@ class FileStoreTest < ActiveSupport::TestCase 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| + basename = File.basename(path) + dirname = File.dirname(path) + Dir::Tmpname.create(basename, Dir.tmpdir + dirname) do |tmpname, n, opts| assert File.basename(tmpname + ".lock").length <= 255, "Temp filename too long: #{File.basename(tmpname + '.lock').length}" end end @@ -98,13 +101,13 @@ 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 Dir.entries(sub_cache_dir).reject { |f| ActiveSupport::Cache::FileStore::EXCLUDED_DIRS.include?(f) }.empty? + assert_empty Dir.children(sub_cache_dir) 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? + assert_predicate @buffer.string, :present? end end diff --git a/activesupport/test/cache/stores/mem_cache_store_test.rb b/activesupport/test/cache/stores/mem_cache_store_test.rb index 99624caf8a..f426a37c66 100644 --- a/activesupport/test/cache/stores/mem_cache_store_test.rb +++ b/activesupport/test/cache/stores/mem_cache_store_test.rb @@ -5,6 +5,24 @@ require "active_support/cache" require_relative "../behaviors" require "dalli" +# Emulates a latency on Dalli's back-end for the key latency to facilitate +# connection pool testing. +class SlowDalliClient < Dalli::Client + def get(key, options = {}) + if key =~ /latency/ + sleep 3 + else + super + end + end +end + +class UnavailableDalliServer < Dalli::Server + def alive? + false + end +end + class MemCacheStoreTest < ActiveSupport::TestCase begin ss = Dalli::Client.new("localhost:11211").stats @@ -31,8 +49,11 @@ class MemCacheStoreTest < ActiveSupport::TestCase include CacheStoreVersionBehavior include LocalCacheBehavior include CacheIncrementDecrementBehavior + include CacheInstrumentationBehavior include EncodedKeyCacheBehavior include AutoloadingCacheBehavior + include ConnectionPoolBehavior + include FailureSafetyBehavior def test_raw_values cache = ActiveSupport::Cache.lookup_store(:mem_cache_store, raw: true) @@ -89,4 +110,30 @@ class MemCacheStoreTest < ActiveSupport::TestCase value << "bingo" assert_not_equal value, @cache.read("foo") end + + private + + def store + :mem_cache_store + end + + def emulating_latency + old_client = Dalli.send(:remove_const, :Client) + Dalli.const_set(:Client, SlowDalliClient) + + yield + ensure + Dalli.send(:remove_const, :Client) + Dalli.const_set(:Client, old_client) + end + + def emulating_unavailability + old_server = Dalli.send(:remove_const, :Server) + Dalli.const_set(:Server, UnavailableDalliServer) + + yield ActiveSupport::Cache::MemCacheStore.new + ensure + Dalli.send(:remove_const, :Server) + Dalli.const_set(:Server, old_server) + end end diff --git a/activesupport/test/cache/stores/memory_store_test.rb b/activesupport/test/cache/stores/memory_store_test.rb index 3981f05331..4c0a4f549d 100644 --- a/activesupport/test/cache/stores/memory_store_test.rb +++ b/activesupport/test/cache/stores/memory_store_test.rb @@ -6,14 +6,21 @@ 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) + @cache = ActiveSupport::Cache.lookup_store(:memory_store, expires_in: 60) end include CacheStoreBehavior include CacheStoreVersionBehavior include CacheDeleteMatchedBehavior include CacheIncrementDecrementBehavior + include CacheInstrumentationBehavior +end + +class MemoryStorePruningTest < 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 def test_prune_size @cache.write(1, "aaaaaaaaaa") && sleep(0.001) @@ -26,9 +33,9 @@ class MemoryStoreTest < ActiveSupport::TestCase @cache.prune(@record_size * 3) assert @cache.exist?(5) assert @cache.exist?(4) - assert !@cache.exist?(3), "no entry" + assert_not @cache.exist?(3), "no entry" assert @cache.exist?(2) - assert !@cache.exist?(1), "no entry" + assert_not @cache.exist?(1), "no entry" end def test_prune_size_on_write @@ -50,12 +57,12 @@ class MemoryStoreTest < ActiveSupport::TestCase 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_not @cache.exist?(6), "no entry" + assert_not @cache.exist?(5), "no entry" assert @cache.exist?(4) - assert !@cache.exist?(3), "no entry" + assert_not @cache.exist?(3), "no entry" assert @cache.exist?(2) - assert !@cache.exist?(1), "no entry" + assert_not @cache.exist?(1), "no entry" end def test_prune_size_on_write_based_on_key_length @@ -75,11 +82,11 @@ class MemoryStoreTest < ActiveSupport::TestCase 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" + assert_not @cache.exist?(5), "no entry" + assert_not @cache.exist?(4), "no entry" + assert_not @cache.exist?(3), "no entry" + assert_not @cache.exist?(2), "no entry" + assert_not @cache.exist?(1), "no entry" end def test_pruning_is_capped_at_a_max_time @@ -97,7 +104,7 @@ class MemoryStoreTest < ActiveSupport::TestCase assert @cache.exist?(4) assert @cache.exist?(3) assert @cache.exist?(2) - assert !@cache.exist?(1) + assert_not @cache.exist?(1) end def test_write_with_unless_exist diff --git a/activesupport/test/cache/stores/redis_cache_store_test.rb b/activesupport/test/cache/stores/redis_cache_store_test.rb index 7f684f7a0f..273b196458 100644 --- a/activesupport/test/cache/stores/redis_cache_store_test.rb +++ b/activesupport/test/cache/stores/redis_cache_store_test.rb @@ -5,7 +5,27 @@ require "active_support/cache" require "active_support/cache/redis_cache_store" require_relative "../behaviors" +driver_name = %w[ ruby hiredis ].include?(ENV["REDIS_DRIVER"]) ? ENV["REDIS_DRIVER"] : "hiredis" +driver = Object.const_get("Redis::Connection::#{driver_name.camelize}") + +Redis::Connection.drivers.clear +Redis::Connection.drivers.append(driver) + +# Emulates a latency on Redis's back-end for the key latency to facilitate +# connection pool testing. +class SlowRedis < Redis + def get(key, options = {}) + if key =~ /latency/ + sleep 3 + else + super + end + end +end + module ActiveSupport::Cache::RedisCacheStoreTests + DRIVER = %w[ ruby hiredis ].include?(ENV["REDIS_DRIVER"]) ? ENV["REDIS_DRIVER"] : "hiredis" + class LookupTest < ActiveSupport::TestCase test "may be looked up as :redis_cache_store" do assert_kind_of ActiveSupport::Cache::RedisCacheStore, @@ -18,7 +38,7 @@ module ActiveSupport::Cache::RedisCacheStoreTests assert_called_with Redis, :new, [ url: nil, connect_timeout: 20, read_timeout: 1, write_timeout: 1, - reconnect_attempts: 0, + reconnect_attempts: 0, driver: DRIVER ] do build end @@ -28,7 +48,7 @@ module ActiveSupport::Cache::RedisCacheStoreTests assert_called_with Redis, :new, [ url: nil, connect_timeout: 20, read_timeout: 1, write_timeout: 1, - reconnect_attempts: 0, + reconnect_attempts: 0, driver: DRIVER ] do build url: [] end @@ -38,7 +58,7 @@ module ActiveSupport::Cache::RedisCacheStoreTests assert_called_with Redis, :new, [ url: "redis://localhost:6379/0", connect_timeout: 20, read_timeout: 1, write_timeout: 1, - reconnect_attempts: 0, + reconnect_attempts: 0, driver: DRIVER ] do build url: "redis://localhost:6379/0" end @@ -48,7 +68,7 @@ module ActiveSupport::Cache::RedisCacheStoreTests assert_called_with Redis, :new, [ url: "redis://localhost:6379/0", connect_timeout: 20, read_timeout: 1, write_timeout: 1, - reconnect_attempts: 0, + reconnect_attempts: 0, driver: DRIVER ] do build url: %w[ redis://localhost:6379/0 ] end @@ -58,10 +78,10 @@ module ActiveSupport::Cache::RedisCacheStoreTests assert_called_with Redis, :new, [ [ url: "redis://localhost:6379/0", connect_timeout: 20, read_timeout: 1, write_timeout: 1, - reconnect_attempts: 0 ], + reconnect_attempts: 0, driver: DRIVER ], [ url: "redis://localhost:6379/1", connect_timeout: 20, read_timeout: 1, write_timeout: 1, - reconnect_attempts: 0 ], + reconnect_attempts: 0, driver: DRIVER ], ], returns: Redis.new do @cache = build url: %w[ redis://localhost:6379/0 redis://localhost:6379/1 ] assert_kind_of ::Redis::Distributed, @cache.redis @@ -75,11 +95,15 @@ module ActiveSupport::Cache::RedisCacheStoreTests end end + test "instance of Redis uses given instance" do + redis_instance = Redis.new + @cache = build(redis: redis_instance) + assert_same @cache.redis, redis_instance + end + private def build(**kwargs) - ActiveSupport::Cache::RedisCacheStore.new(**kwargs).tap do |cache| - cache.redis - end + ActiveSupport::Cache::RedisCacheStore.new(driver: DRIVER, **kwargs).tap(&:redis) end end @@ -87,11 +111,11 @@ module ActiveSupport::Cache::RedisCacheStoreTests setup do @namespace = "namespace" - @cache = ActiveSupport::Cache::RedisCacheStore.new(timeout: 0.1, namespace: @namespace, expires_in: 60) + @cache = ActiveSupport::Cache::RedisCacheStore.new(timeout: 0.1, namespace: @namespace, expires_in: 60, driver: DRIVER) # @cache.logger = Logger.new($stdout) # For test debugging # For LocalCacheBehavior tests - @peek = ActiveSupport::Cache::RedisCacheStore.new(timeout: 0.1, namespace: @namespace) + @peek = ActiveSupport::Cache::RedisCacheStore.new(timeout: 0.1, namespace: @namespace, driver: DRIVER) end teardown do @@ -105,7 +129,83 @@ module ActiveSupport::Cache::RedisCacheStoreTests include CacheStoreVersionBehavior include LocalCacheBehavior include CacheIncrementDecrementBehavior + include CacheInstrumentationBehavior include AutoloadingCacheBehavior + + def test_fetch_multi_uses_redis_mget + assert_called(@cache.redis, :mget, returns: []) do + @cache.fetch_multi("a", "b", "c") do |key| + key * 2 + end + 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 + @cache.increment "foo", 1, expires_in: 60 + end + end + + # key and ttl exist + @cache.redis.setex "#{@namespace}:bar", 120, 1 + assert_not_called @cache.redis, :expire do + @cache.increment "bar", 1, expires_in: 2.minutes + end + + # key exist but not have expire + @cache.redis.set "#{@namespace}:dar", 10 + assert_called_with @cache.redis, :expire, [ "#{@namespace}:dar", 60 ] do + @cache.increment "dar", 1, expires_in: 60 + end + end + + def test_decrement_expires_in + assert_called_with @cache.redis, :decrby, [ "#{@namespace}:foo", 1 ] do + assert_called_with @cache.redis, :expire, [ "#{@namespace}:foo", 60 ] do + @cache.decrement "foo", 1, expires_in: 60 + end + end + + # key and ttl exist + @cache.redis.setex "#{@namespace}:bar", 120, 1 + assert_not_called @cache.redis, :expire do + @cache.decrement "bar", 1, expires_in: 2.minutes + end + + # key exist but not have expire + @cache.redis.set "#{@namespace}:dar", 10 + assert_called_with @cache.redis, :expire, [ "#{@namespace}:dar", 60 ] do + @cache.decrement "dar", 1, expires_in: 60 + end + end + end + + class ConnectionPoolBehaviourTest < StoreTest + include ConnectionPoolBehavior + + private + + def store + :redis_cache_store + end + + def emulating_latency + old_redis = Object.send(:remove_const, :Redis) + Object.const_set(:Redis, SlowRedis) + + yield + ensure + Object.send(:remove_const, :Redis) + Object.const_set(:Redis, old_redis) + end + end + + class RedisDistributedConnectionPoolBehaviourTest < ConnectionPoolBehaviourTest + private + def store_options + { url: [ENV["REDIS_URL"] || "redis://localhost:6379/0"] * 2 } + end end # Separate test class so we can omit the namespace which causes expected, @@ -114,7 +214,7 @@ module ActiveSupport::Cache::RedisCacheStoreTests include EncodedKeyCacheBehavior setup do - @cache = ActiveSupport::Cache::RedisCacheStore.new(timeout: 0.1) + @cache = ActiveSupport::Cache::RedisCacheStore.new(timeout: 0.1, driver: DRIVER) @cache.logger = nil end end @@ -122,15 +222,26 @@ module ActiveSupport::Cache::RedisCacheStoreTests class StoreAPITest < StoreTest end - class FailureSafetyTest < StoreTest - test "fetch read failure returns nil" do + class UnavailableRedisClient < Redis::Client + def ensure_connected + raise Redis::BaseConnectionError end + end - test "fetch read failure does not attempt to write" do - end + class FailureSafetyTest < StoreTest + include FailureSafetyBehavior - test "write failure returns nil" do - end + private + + def emulating_unavailability + old_client = Redis.send(:remove_const, :Client) + Redis.const_set(:Client, UnavailableRedisClient) + + yield ActiveSupport::Cache::RedisCacheStore.new + ensure + Redis.send(:remove_const, :Client) + Redis.const_set(:Client, old_client) + end end class DeleteMatchedTest < StoreTest @@ -138,7 +249,7 @@ module ActiveSupport::Cache::RedisCacheStoreTests @cache.write("foo", "bar") @cache.write("fu", "baz") @cache.delete_matched("foo*") - assert !@cache.exist?("foo") + assert_not @cache.exist?("foo") assert @cache.exist?("fu") end @@ -148,4 +259,22 @@ module ActiveSupport::Cache::RedisCacheStoreTests end end end + + class ClearTest < StoreTest + test "clear all cache key" do + @cache.write("foo", "bar") + @cache.write("fu", "baz") + @cache.clear + assert_not @cache.exist?("foo") + assert_not @cache.exist?("fu") + end + + test "only clear namespace cache key" do + @cache.write("foo", "bar") + @cache.redis.set("fu", "baz") + @cache.clear + assert_not @cache.exist?("foo") + assert @cache.redis.exists("fu") + end + end end diff --git a/activesupport/test/callback_inheritance_test.rb b/activesupport/test/callback_inheritance_test.rb index 67813a749e..5633b6e2b8 100644 --- a/activesupport/test/callback_inheritance_test.rb +++ b/activesupport/test/callback_inheritance_test.rb @@ -164,10 +164,10 @@ end class DynamicInheritedCallbacks < ActiveSupport::TestCase def test_callbacks_looks_to_the_superclass_before_running child = EmptyChild.new.dispatch - assert !child.performed? + assert_not_predicate child, :performed? EmptyParent.set_callback :dispatch, :before, :perform! child = EmptyChild.new.dispatch - assert child.performed? + assert_predicate child, :performed? end def test_callbacks_should_be_performed_once_in_child_class @@ -176,3 +176,13 @@ class DynamicInheritedCallbacks < ActiveSupport::TestCase assert_equal 1, child.count end end + +class DynamicDefinedCallbacks < ActiveSupport::TestCase + def test_callbacks_should_be_performed_once_in_child_class_after_dynamic_define + GrandParent.define_callbacks(:foo) + GrandParent.set_callback(:foo, :before, :before1) + parent = Parent.new("foo") + parent.run_callbacks(:foo) + assert_equal %w(before1), parent.log + end +end diff --git a/activesupport/test/callbacks_test.rb b/activesupport/test/callbacks_test.rb index 30f1632460..79098b2a7d 100644 --- a/activesupport/test/callbacks_test.rb +++ b/activesupport/test/callbacks_test.rb @@ -31,7 +31,7 @@ module CallbacksTest def callback_object(callback_method) klass = Class.new - klass.send(:define_method, callback_method) do |model| + klass.define_method(callback_method) do |model| model.history << [:"#{callback_method}_save", :object] end klass.new @@ -482,10 +482,9 @@ module CallbacksTest "block in run_callbacks", "tweedle_dum", "block in run_callbacks", - ("call" if RUBY_VERSION < "2.3"), "run_callbacks", "save" - ].compact, call_stack.map(&:label) + ], call_stack.map(&:label) end def test_short_call_stack @@ -830,7 +829,7 @@ module CallbacksTest def test_block_never_called_if_terminated obj = CallbackTerminator.new obj.save - assert !obj.saved + assert_not obj.saved end end @@ -858,7 +857,7 @@ module CallbacksTest def test_block_never_called_if_abort_is_thrown obj = CallbackDefaultTerminator.new obj.save - assert !obj.saved + assert_not obj.saved end end @@ -954,7 +953,7 @@ module CallbacksTest def test_proc_arity_2 assert_raises(ArgumentError) do - klass = build_class(->(x, y) {}) + klass = build_class(->(x, y) { }) klass.new.run end end @@ -1033,7 +1032,7 @@ module CallbacksTest def test_proc_arity2 assert_raises(ArgumentError) do - object = build_class(->(a, b) {}).new + object = build_class(->(a, b) { }).new object.run end end diff --git a/activesupport/test/class_cache_test.rb b/activesupport/test/class_cache_test.rb index 7b97028e8c..1ef1939b4b 100644 --- a/activesupport/test/class_cache_test.rb +++ b/activesupport/test/class_cache_test.rb @@ -11,17 +11,17 @@ module ActiveSupport end def test_empty? - assert @cache.empty? + assert_empty @cache @cache.store(ClassCacheTest) - assert !@cache.empty? + assert_not_empty @cache end def test_clear! - assert @cache.empty? + assert_empty @cache @cache.store(ClassCacheTest) - assert !@cache.empty? + assert_not_empty @cache @cache.clear! - assert @cache.empty? + assert_empty @cache end def test_set_key @@ -40,35 +40,35 @@ module ActiveSupport end def test_get_constantizes - assert @cache.empty? + assert_empty @cache assert_equal ClassCacheTest, @cache.get(ClassCacheTest.name) end def test_get_constantizes_fails_on_invalid_names - assert @cache.empty? + assert_empty @cache assert_raise NameError do @cache.get("OmgTotallyInvalidConstantName") end end def test_get_alias - assert @cache.empty? + assert_empty @cache assert_equal @cache[ClassCacheTest.name], @cache.get(ClassCacheTest.name) end def test_safe_get_constantizes - assert @cache.empty? + assert_empty @cache assert_equal ClassCacheTest, @cache.safe_get(ClassCacheTest.name) end def test_safe_get_constantizes_doesnt_fail_on_invalid_names - assert @cache.empty? + assert_empty @cache assert_nil @cache.safe_get("OmgTotallyInvalidConstantName") end def test_new_rejects_strings @cache.store ClassCacheTest.name - assert !@cache.key?(ClassCacheTest.name) + assert_not @cache.key?(ClassCacheTest.name) end def test_store_returns_self diff --git a/activesupport/test/clean_backtrace_test.rb b/activesupport/test/clean_backtrace_test.rb index 1b44c7c9bf..a0a7056952 100644 --- a/activesupport/test/clean_backtrace_test.rb +++ b/activesupport/test/clean_backtrace_test.rb @@ -74,3 +74,43 @@ class BacktraceCleanerFilterAndSilencerTest < ActiveSupport::TestCase assert_equal [ "/class.rb" ], @bc.clean([ "/mongrel/class.rb" ]) end end + +class BacktraceCleanerDefaultFilterAndSilencerTest < ActiveSupport::TestCase + def setup + @bc = ActiveSupport::BacktraceCleaner.new + end + + test "should format installed gems correctly" do + backtrace = [ "#{Gem.default_dir}/gems/nosuchgem-1.2.3/lib/foo.rb" ] + result = @bc.clean(backtrace, :all) + assert_equal "nosuchgem (1.2.3) lib/foo.rb", result[0] + end + + test "should format installed gems not in Gem.default_dir correctly" do + target_dir = Gem.path.detect { |p| p != Gem.default_dir } + # skip this test if default_dir is the only directory on Gem.path + if target_dir + backtrace = [ "#{target_dir}/gems/nosuchgem-1.2.3/lib/foo.rb" ] + result = @bc.clean(backtrace, :all) + assert_equal "nosuchgem (1.2.3) lib/foo.rb", result[0] + end + end + + test "should format gems installed by bundler" do + backtrace = [ "#{Gem.default_dir}/bundler/gems/nosuchgem-1.2.3/lib/foo.rb" ] + result = @bc.clean(backtrace, :all) + assert_equal "nosuchgem (1.2.3) lib/foo.rb", result[0] + end + + test "should silence gems from the backtrace" do + backtrace = [ "#{Gem.path[0]}/gems/nosuchgem-1.2.3/lib/foo.rb" ] + result = @bc.clean(backtrace) + assert_empty result + end + + test "should silence stdlib" do + backtrace = ["#{RbConfig::CONFIG["rubylibdir"]}/lib/foo.rb"] + result = @bc.clean(backtrace) + assert_empty result + end +end diff --git a/activesupport/test/concern_test.rb b/activesupport/test/concern_test.rb index ef75a320d1..4b3cfcd1d2 100644 --- a/activesupport/test/concern_test.rb +++ b/activesupport/test/concern_test.rb @@ -91,7 +91,7 @@ class ConcernTest < ActiveSupport::TestCase end end @klass.include test_module - assert_equal false, Object.respond_to?(:test) + assert_not_respond_to Object, :test Qux.class_eval do remove_const :ClassMethods end @@ -128,4 +128,12 @@ class ConcernTest < ActiveSupport::TestCase end end end + + def test_no_raise_on_same_included_call + assert_nothing_raised do + 2.times do + load File.expand_path("../fixtures/concern/some_concern.rb", __FILE__) + end + end + end end diff --git a/activesupport/test/configurable_test.rb b/activesupport/test/configurable_test.rb index 10719596df..1cf40261dc 100644 --- a/activesupport/test/configurable_test.rb +++ b/activesupport/test/configurable_test.rb @@ -43,11 +43,11 @@ class ConfigurableActiveSupport < ActiveSupport::TestCase test "configuration accessors are not available on instance" do instance = Parent.new - assert !instance.respond_to?(:bar) - assert !instance.respond_to?(:bar=) + assert_not_respond_to instance, :bar + assert_not_respond_to instance, :bar= - assert !instance.respond_to?(:baz) - assert !instance.respond_to?(:baz=) + assert_not_respond_to instance, :baz + assert_not_respond_to instance, :baz= end test "configuration accessors can take a default value" do diff --git a/activesupport/test/constantize_test_cases.rb b/activesupport/test/constantize_test_cases.rb index 2c6145940b..cdb8441b81 100644 --- a/activesupport/test/constantize_test_cases.rb +++ b/activesupport/test/constantize_test_cases.rb @@ -112,6 +112,16 @@ module ConstantizeTestCases assert_nil yield("A::Object::B") assert_nil yield("A::Object::Object::Object::B") + with_autoloading_fixtures do + assert_nil yield("Em") + end + + assert_raises(LoadError) do + with_autoloading_fixtures do + yield("RaisesLoadError") + end + end + assert_raises(NameError) do with_autoloading_fixtures do yield("RaisesNameError") diff --git a/activesupport/test/core_ext/array/extract_test.rb b/activesupport/test/core_ext/array/extract_test.rb new file mode 100644 index 0000000000..f26e055033 --- /dev/null +++ b/activesupport/test/core_ext/array/extract_test.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +require "abstract_unit" +require "active_support/core_ext/array" + +class ExtractTest < ActiveSupport::TestCase + def test_extract + numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + array_id = numbers.object_id + + odd_numbers = numbers.extract!(&:odd?) + + assert_equal [1, 3, 5, 7, 9], odd_numbers + assert_equal [0, 2, 4, 6, 8], numbers + assert_equal array_id, numbers.object_id + end + + def test_extract_without_block + numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + array_id = numbers.object_id + + extract_enumerator = numbers.extract! + + assert_instance_of Enumerator, extract_enumerator + assert_equal numbers.size, extract_enumerator.size + + odd_numbers = extract_enumerator.each(&:odd?) + + assert_equal [1, 3, 5, 7, 9], odd_numbers + assert_equal [0, 2, 4, 6, 8], numbers + assert_equal array_id, numbers.object_id + end + + def test_extract_on_empty_array + empty_array = [] + array_id = empty_array.object_id + + new_empty_array = empty_array.extract! { } + + assert_equal [], new_empty_array + assert_equal [], empty_array + assert_equal array_id, empty_array.object_id + end +end diff --git a/activesupport/test/core_ext/array/grouping_test.rb b/activesupport/test/core_ext/array/grouping_test.rb index c182b91826..37111a5d7d 100644 --- a/activesupport/test/core_ext/array/grouping_test.rb +++ b/activesupport/test/core_ext/array/grouping_test.rb @@ -4,15 +4,6 @@ require "abstract_unit" require "active_support/core_ext/array" class GroupingTest < ActiveSupport::TestCase - def setup - # In Ruby < 2.4, test we avoid Integer#/ (redefined by mathn) - Fixnum.send :private, :/ unless 0.class == Integer - end - - def teardown - Fixnum.send :public, :/ unless 0.class == Integer - end - def test_in_groups_of_with_perfect_fit groups = [] ("a".."i").to_a.in_groups_of(3) do |group| diff --git a/activesupport/test/core_ext/array/prepend_append_test.rb b/activesupport/test/core_ext/array/prepend_append_test.rb index c34acd66ad..8573dbd5a6 100644 --- a/activesupport/test/core_ext/array/prepend_append_test.rb +++ b/activesupport/test/core_ext/array/prepend_append_test.rb @@ -1,14 +1,11 @@ # frozen_string_literal: true require "abstract_unit" -require "active_support/core_ext/array" class PrependAppendTest < ActiveSupport::TestCase - def test_append - assert_equal [1, 2], [1].append(2) - end - - def test_prepend - assert_equal [2, 1], [1].prepend(2) + def test_requiring_prepend_and_append_is_deprecated + assert_deprecated do + require "active_support/core_ext/array/prepend_and_append" + end end end diff --git a/activesupport/test/core_ext/class_test.rb b/activesupport/test/core_ext/class_test.rb index 9cc006fc63..5ea288738e 100644 --- a/activesupport/test/core_ext/class_test.rb +++ b/activesupport/test/core_ext/class_test.rb @@ -30,11 +30,11 @@ class ClassTest < ActiveSupport::TestCase def test_descendants_excludes_singleton_classes klass = Parent.new.singleton_class - refute Parent.descendants.include?(klass), "descendants should not include singleton classes" + assert_not Parent.descendants.include?(klass), "descendants should not include singleton classes" end def test_subclasses_excludes_singleton_classes klass = Parent.new.singleton_class - refute Parent.subclasses.include?(klass), "subclasses should not include singleton classes" + assert_not Parent.subclasses.include?(klass), "subclasses should not include singleton classes" end end diff --git a/activesupport/test/core_ext/date_and_time_behavior.rb b/activesupport/test/core_ext/date_and_time_behavior.rb index 1176ed647a..b77ea22701 100644 --- a/activesupport/test/core_ext/date_and_time_behavior.rb +++ b/activesupport/test/core_ext/date_and_time_behavior.rb @@ -361,28 +361,40 @@ module DateAndTimeBehavior end def test_on_weekend_on_saturday - assert date_time_init(2015, 1, 3, 0, 0, 0).on_weekend? - assert date_time_init(2015, 1, 3, 15, 15, 10).on_weekend? + assert_predicate date_time_init(2015, 1, 3, 0, 0, 0), :on_weekend? + assert_predicate date_time_init(2015, 1, 3, 15, 15, 10), :on_weekend? end def test_on_weekend_on_sunday - assert date_time_init(2015, 1, 4, 0, 0, 0).on_weekend? - assert date_time_init(2015, 1, 4, 15, 15, 10).on_weekend? + assert_predicate date_time_init(2015, 1, 4, 0, 0, 0), :on_weekend? + assert_predicate date_time_init(2015, 1, 4, 15, 15, 10), :on_weekend? end def test_on_weekend_on_monday - assert_not date_time_init(2015, 1, 5, 0, 0, 0).on_weekend? - assert_not date_time_init(2015, 1, 5, 15, 15, 10).on_weekend? + assert_not_predicate date_time_init(2015, 1, 5, 0, 0, 0), :on_weekend? + assert_not_predicate date_time_init(2015, 1, 5, 15, 15, 10), :on_weekend? end def test_on_weekday_on_sunday - assert_not date_time_init(2015, 1, 4, 0, 0, 0).on_weekday? - assert_not date_time_init(2015, 1, 4, 15, 15, 10).on_weekday? + assert_not_predicate date_time_init(2015, 1, 4, 0, 0, 0), :on_weekday? + assert_not_predicate date_time_init(2015, 1, 4, 15, 15, 10), :on_weekday? end def test_on_weekday_on_monday - assert date_time_init(2015, 1, 5, 0, 0, 0).on_weekday? - assert date_time_init(2015, 1, 5, 15, 15, 10).on_weekday? + assert_predicate date_time_init(2015, 1, 5, 0, 0, 0), :on_weekday? + assert_predicate date_time_init(2015, 1, 5, 15, 15, 10), :on_weekday? + end + + def test_before + assert_equal false, date_time_init(2017, 3, 6, 12, 0, 0).before?(date_time_init(2017, 3, 5, 12, 0, 0)) + assert_equal false, date_time_init(2017, 3, 6, 12, 0, 0).before?(date_time_init(2017, 3, 6, 12, 0, 0)) + assert_equal true, date_time_init(2017, 3, 6, 12, 0, 0).before?(date_time_init(2017, 3, 7, 12, 0, 0)) + end + + def test_after + assert_equal true, date_time_init(2017, 3, 6, 12, 0, 0).after?(date_time_init(2017, 3, 5, 12, 0, 0)) + assert_equal false, date_time_init(2017, 3, 6, 12, 0, 0).after?(date_time_init(2017, 3, 6, 12, 0, 0)) + assert_equal false, date_time_init(2017, 3, 6, 12, 0, 0).after?(date_time_init(2017, 3, 7, 12, 0, 0)) end def with_bw_default(bw = :monday) diff --git a/activesupport/test/core_ext/date_and_time_compatibility_test.rb b/activesupport/test/core_ext/date_and_time_compatibility_test.rb index 266829a452..58a24b60b6 100644 --- a/activesupport/test/core_ext/date_and_time_compatibility_test.rb +++ b/activesupport/test/core_ext/date_and_time_compatibility_test.rb @@ -248,7 +248,7 @@ class DateAndTimeCompatibilityTest < ActiveSupport::TestCase def test_string_to_time_frozen_preserves_timezone with_preserve_timezone(true) do with_env_tz "US/Eastern" do - source = "2016-04-23T15:11:12+01:00".freeze + source = "2016-04-23T15:11:12+01:00" time = source.to_time assert_instance_of Time, time @@ -262,7 +262,7 @@ class DateAndTimeCompatibilityTest < ActiveSupport::TestCase def test_string_to_time_frozen_does_not_preserve_time_zone with_preserve_timezone(false) do with_env_tz "US/Eastern" do - source = "2016-04-23T15:11:12+01:00".freeze + source = "2016-04-23T15:11:12+01:00" time = source.to_time assert_instance_of Time, time diff --git a/activesupport/test/core_ext/date_ext_test.rb b/activesupport/test/core_ext/date_ext_test.rb index 23d17956df..b8652884ce 100644 --- a/activesupport/test/core_ext/date_ext_test.rb +++ b/activesupport/test/core_ext/date_ext_test.rb @@ -386,11 +386,11 @@ end class DateExtBehaviorTest < ActiveSupport::TestCase def test_date_acts_like_date - assert Date.new.acts_like_date? + assert_predicate Date.new, :acts_like_date? end def test_blank? - assert_not Date.new.blank? + assert_not_predicate Date.new, :blank? end def test_freeze_doesnt_clobber_memoized_instance_methods diff --git a/activesupport/test/core_ext/date_time_ext_test.rb b/activesupport/test/core_ext/date_time_ext_test.rb index f4c9dfcb25..f9f6b21c9b 100644 --- a/activesupport/test/core_ext/date_time_ext_test.rb +++ b/activesupport/test/core_ext/date_time_ext_test.rb @@ -152,8 +152,8 @@ class DateTimeExtCalculationsTest < ActiveSupport::TestCase assert_equal DateTime.civil(2005, 2, 22, 11, 10, 10), DateTime.civil(2005, 2, 22, 10, 10, 10).since(3600) assert_equal DateTime.civil(2005, 2, 24, 10, 10, 10), DateTime.civil(2005, 2, 22, 10, 10, 10).since(86400 * 2) assert_equal DateTime.civil(2005, 2, 24, 11, 10, 35), DateTime.civil(2005, 2, 22, 10, 10, 10).since(86400 * 2 + 3600 + 25) - assert_equal DateTime.civil(2005, 2, 22, 10, 10, 11), DateTime.civil(2005, 2, 22, 10, 10, 10).since(1.333) - assert_equal DateTime.civil(2005, 2, 22, 10, 10, 12), DateTime.civil(2005, 2, 22, 10, 10, 10).since(1.667) + assert_not_equal DateTime.civil(2005, 2, 22, 10, 10, 11), DateTime.civil(2005, 2, 22, 10, 10, 10).since(1.333) + assert_not_equal DateTime.civil(2005, 2, 22, 10, 10, 12), DateTime.civil(2005, 2, 22, 10, 10, 10).since(1.667) end def test_change @@ -315,15 +315,15 @@ class DateTimeExtCalculationsTest < ActiveSupport::TestCase end def test_acts_like_date - assert DateTime.new.acts_like_date? + assert_predicate DateTime.new, :acts_like_date? end def test_acts_like_time - assert DateTime.new.acts_like_time? + assert_predicate DateTime.new, :acts_like_time? end def test_blank? - assert_not DateTime.new.blank? + assert_not_predicate DateTime.new, :blank? end def test_utc? @@ -363,45 +363,45 @@ class DateTimeExtCalculationsTest < ActiveSupport::TestCase end def test_compare_with_time - assert_equal 1, DateTime.civil(2000) <=> Time.utc(1999, 12, 31, 23, 59, 59) - assert_equal 0, DateTime.civil(2000) <=> Time.utc(2000, 1, 1, 0, 0, 0) + assert_equal 1, DateTime.civil(2000) <=> Time.utc(1999, 12, 31, 23, 59, 59) + assert_equal 0, DateTime.civil(2000) <=> Time.utc(2000, 1, 1, 0, 0, 0) assert_equal(-1, DateTime.civil(2000) <=> Time.utc(2000, 1, 1, 0, 0, 1)) end def test_compare_with_datetime - assert_equal 1, DateTime.civil(2000) <=> DateTime.civil(1999, 12, 31, 23, 59, 59) - assert_equal 0, DateTime.civil(2000) <=> DateTime.civil(2000, 1, 1, 0, 0, 0) + assert_equal 1, DateTime.civil(2000) <=> DateTime.civil(1999, 12, 31, 23, 59, 59) + assert_equal 0, DateTime.civil(2000) <=> DateTime.civil(2000, 1, 1, 0, 0, 0) assert_equal(-1, DateTime.civil(2000) <=> DateTime.civil(2000, 1, 1, 0, 0, 1)) end def test_compare_with_time_with_zone - assert_equal 1, DateTime.civil(2000) <=> ActiveSupport::TimeWithZone.new(Time.utc(1999, 12, 31, 23, 59, 59), ActiveSupport::TimeZone["UTC"]) - assert_equal 0, DateTime.civil(2000) <=> ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 1, 0, 0, 0), ActiveSupport::TimeZone["UTC"]) + assert_equal 1, DateTime.civil(2000) <=> ActiveSupport::TimeWithZone.new(Time.utc(1999, 12, 31, 23, 59, 59), ActiveSupport::TimeZone["UTC"]) + assert_equal 0, DateTime.civil(2000) <=> ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 1, 0, 0, 0), ActiveSupport::TimeZone["UTC"]) assert_equal(-1, DateTime.civil(2000) <=> ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 1, 0, 0, 1), ActiveSupport::TimeZone["UTC"])) end def test_compare_with_string - assert_equal 1, DateTime.civil(2000) <=> Time.utc(1999, 12, 31, 23, 59, 59).to_s - assert_equal 0, DateTime.civil(2000) <=> Time.utc(2000, 1, 1, 0, 0, 0).to_s + assert_equal 1, DateTime.civil(2000) <=> Time.utc(1999, 12, 31, 23, 59, 59).to_s + assert_equal 0, DateTime.civil(2000) <=> Time.utc(2000, 1, 1, 0, 0, 0).to_s assert_equal(-1, DateTime.civil(2000) <=> Time.utc(2000, 1, 1, 0, 0, 1).to_s) assert_nil DateTime.civil(2000) <=> "Invalid as Time" end def test_compare_with_integer - assert_equal 1, DateTime.civil(1970, 1, 1, 12, 0, 0) <=> 2440587 - assert_equal 0, DateTime.civil(1970, 1, 1, 12, 0, 0) <=> 2440588 + assert_equal 1, DateTime.civil(1970, 1, 1, 12, 0, 0) <=> 2440587 + assert_equal 0, DateTime.civil(1970, 1, 1, 12, 0, 0) <=> 2440588 assert_equal(-1, DateTime.civil(1970, 1, 1, 12, 0, 0) <=> 2440589) end def test_compare_with_float - assert_equal 1, DateTime.civil(1970) <=> 2440586.5 - assert_equal 0, DateTime.civil(1970) <=> 2440587.5 + assert_equal 1, DateTime.civil(1970) <=> 2440586.5 + assert_equal 0, DateTime.civil(1970) <=> 2440587.5 assert_equal(-1, DateTime.civil(1970) <=> 2440588.5) end def test_compare_with_rational - assert_equal 1, DateTime.civil(1970) <=> Rational(4881173, 2) - assert_equal 0, DateTime.civil(1970) <=> Rational(4881175, 2) + assert_equal 1, DateTime.civil(1970) <=> Rational(4881173, 2) + assert_equal 0, DateTime.civil(1970) <=> Rational(4881175, 2) assert_equal(-1, DateTime.civil(1970) <=> Rational(4881177, 2)) end diff --git a/activesupport/test/core_ext/duration_test.rb b/activesupport/test/core_ext/duration_test.rb index dbee543644..63934e2433 100644 --- a/activesupport/test/core_ext/duration_test.rb +++ b/activesupport/test/core_ext/duration_test.rb @@ -5,6 +5,7 @@ require "active_support/inflector" require "active_support/time" require "active_support/json" require "time_zone_test_helpers" +require "yaml" class DurationTest < ActiveSupport::TestCase include TimeZoneTestHelpers @@ -15,30 +16,30 @@ class DurationTest < ActiveSupport::TestCase assert_kind_of ActiveSupport::Duration, d assert_kind_of Numeric, d assert_kind_of Integer, d - assert !d.is_a?(Hash) + assert_not d.is_a?(Hash) k = Class.new class << k; undef_method :== end - assert !d.is_a?(k) + assert_not d.is_a?(k) end def test_instance_of - assert 1.minute.instance_of?(1.class) + assert 1.minute.instance_of?(Integer) assert 2.days.instance_of?(ActiveSupport::Duration) - assert !3.second.instance_of?(Numeric) + assert_not 3.second.instance_of?(Numeric) end def test_threequals assert ActiveSupport::Duration === 1.day - assert !(ActiveSupport::Duration === 1.day.to_i) - assert !(ActiveSupport::Duration === "foo") + assert_not (ActiveSupport::Duration === 1.day.to_i) + assert_not (ActiveSupport::Duration === "foo") end def test_equals assert 1.day == 1.day assert 1.day == 1.day.to_i assert 1.day.to_i == 1.day - assert !(1.day == "foo") + assert_not (1.day == "foo") end def test_to_s @@ -52,11 +53,11 @@ class DurationTest < ActiveSupport::TestCase assert 1.minute.eql?(1.minute) assert 1.minute.eql?(60.seconds) assert 2.days.eql?(48.hours) - assert !1.second.eql?(1) - assert !1.eql?(1.second) + assert_not 1.second.eql?(1) + assert_not 1.eql?(1.second) assert 1.minute.eql?(180.seconds - 2.minutes) - assert !1.minute.eql?(60) - assert !1.minute.eql?("foo") + assert_not 1.minute.eql?(60) + assert_not 1.minute.eql?("foo") end def test_inspect @@ -71,6 +72,8 @@ class DurationTest < ActiveSupport::TestCase assert_equal "7 days", 7.days.inspect assert_equal "1 week", 1.week.inspect assert_equal "2 weeks", 1.fortnight.inspect + assert_equal "0 seconds", (10 % 5.seconds).inspect + assert_equal "10 minutes", (10.minutes + 0.seconds).inspect end def test_inspect_locale @@ -155,6 +158,18 @@ class DurationTest < ActiveSupport::TestCase assert_equal Date.civil(2017, 1, 3), Date.civil(2017, 1, 1) + 1.day * 2 end + def test_date_added_with_multiplied_duration_larger_than_one_month + assert_equal Date.civil(2017, 2, 15), Date.civil(2017, 1, 1) + 1.day * 45 + end + + def test_date_added_with_divided_duration + assert_equal Date.civil(2017, 1, 3), Date.civil(2017, 1, 1) + 4.days / 2 + end + + def test_date_added_with_divided_duration_larger_than_one_month + assert_equal Date.civil(2017, 2, 15), Date.civil(2017, 1, 1) + 90.days / 2 + end + def test_plus_with_time assert_equal 1 + 1.second, 1.second + 1, "Duration + Numeric should == Numeric + Duration" end @@ -640,6 +655,12 @@ class DurationTest < ActiveSupport::TestCase assert_equal time + d1, time + d2 end + def test_durations_survive_yaml_serialization + d1 = YAML.load(YAML.dump(10.minutes)) + assert_equal 600, d1.to_i + assert_equal 660, (d1 + 60).to_i + end + private def eastern_time_zone if Gem.win_platform? diff --git a/activesupport/test/core_ext/enumerable_test.rb b/activesupport/test/core_ext/enumerable_test.rb index 8d71320931..b63464a36a 100644 --- a/activesupport/test/core_ext/enumerable_test.rb +++ b/activesupport/test/core_ext/enumerable_test.rb @@ -179,6 +179,21 @@ class EnumerableTests < ActiveSupport::TestCase payments.index_by.each(&:price)) end + def test_index_with + payments = GenericEnumerable.new([ Payment.new(5), Payment.new(15), Payment.new(10) ]) + + assert_equal({ Payment.new(5) => 5, Payment.new(15) => 15, Payment.new(10) => 10 }, payments.index_with(&:price)) + + assert_equal({ title: nil, body: nil }, %i( title body ).index_with(nil)) + assert_equal({ title: [], body: [] }, %i( title body ).index_with([])) + assert_equal({ title: {}, body: {} }, %i( title body ).index_with({})) + + assert_equal Enumerator, payments.index_with.class + assert_nil payments.index_with.size + assert_equal 42, (1..42).index_with.size + assert_equal({ Payment.new(5) => 5, Payment.new(15) => 15, Payment.new(10) => 10 }, payments.index_with.each(&:price)) + end + def test_many assert_equal false, GenericEnumerable.new([]).many? assert_equal false, GenericEnumerable.new([ 1 ]).many? diff --git a/activesupport/test/core_ext/file_test.rb b/activesupport/test/core_ext/file_test.rb index 23e3c277cc..186c863f91 100644 --- a/activesupport/test/core_ext/file_test.rb +++ b/activesupport/test/core_ext/file_test.rb @@ -8,7 +8,7 @@ class AtomicWriteTest < ActiveSupport::TestCase contents = "Atomic Text" File.atomic_write(file_name, Dir.pwd) do |file| file.write(contents) - assert !File.exist?(file_name) + assert_not File.exist?(file_name) end assert File.exist?(file_name) assert_equal contents, File.read(file_name) @@ -22,7 +22,7 @@ class AtomicWriteTest < ActiveSupport::TestCase raise "something bad" end rescue - assert !File.exist?(file_name) + assert_not File.exist?(file_name) end def test_atomic_write_preserves_file_permissions @@ -50,7 +50,7 @@ class AtomicWriteTest < ActiveSupport::TestCase contents = "Atomic Text" File.atomic_write(file_name, Dir.pwd) do |file| file.write(contents) - assert !File.exist?(file_name) + assert_not File.exist?(file_name) end assert File.exist?(file_name) assert_equal File.probe_stat_in(Dir.pwd).mode, file_mode @@ -59,6 +59,20 @@ class AtomicWriteTest < ActiveSupport::TestCase File.unlink(file_name) rescue nil end + def test_atomic_write_preserves_file_permissions_same_directory + Dir.mktmpdir do |temp_dir| + File.chmod 0700, temp_dir + + probed_permissions = File.probe_stat_in(temp_dir).mode.to_s(8) + + File.atomic_write(File.join(temp_dir, file_name), &:close) + + actual_permissions = File.stat(File.join(temp_dir, file_name)).mode.to_s(8) + + assert_equal actual_permissions, probed_permissions + end + end + def test_atomic_write_returns_result_from_yielded_block block_return_value = File.atomic_write(file_name, Dir.pwd) do |file| "Hello world!" diff --git a/activesupport/test/core_ext/hash/transform_keys_test.rb b/activesupport/test/core_ext/hash/transform_keys_test.rb deleted file mode 100644 index b9e41f7b25..0000000000 --- a/activesupport/test/core_ext/hash/transform_keys_test.rb +++ /dev/null @@ -1,64 +0,0 @@ -# frozen_string_literal: true - -require "abstract_unit" -require "active_support/core_ext/hash/keys" - -class TransformKeysTest < ActiveSupport::TestCase - test "transform_keys returns a new hash with the keys computed from the block" do - original = { a: "a", b: "b" } - mapped = original.transform_keys { |k| "#{k}!".to_sym } - - assert_equal({ a: "a", b: "b" }, original) - assert_equal({ a!: "a", b!: "b" }, mapped) - end - - test "transform_keys! modifies the keys of the original" do - original = { a: "a", b: "b" } - mapped = original.transform_keys! { |k| "#{k}!".to_sym } - - assert_equal({ a!: "a", b!: "b" }, original) - assert_same original, mapped - end - - test "transform_keys returns a sized Enumerator if no block is given" do - original = { a: "a", b: "b" } - enumerator = original.transform_keys - assert_equal original.size, enumerator.size - assert_equal Enumerator, enumerator.class - end - - test "transform_keys! returns a sized Enumerator if no block is given" do - original = { a: "a", b: "b" } - enumerator = original.transform_keys! - assert_equal original.size, enumerator.size - assert_equal Enumerator, enumerator.class - end - - test "transform_keys is chainable with Enumerable methods" do - original = { a: "a", b: "b" } - mapped = original.transform_keys.with_index { |k, i| [k, i].join.to_sym } - assert_equal({ a0: "a", b1: "b" }, mapped) - end - - test "transform_keys! is chainable with Enumerable methods" do - original = { a: "a", b: "b" } - original.transform_keys!.with_index { |k, i| [k, i].join.to_sym } - assert_equal({ a0: "a", b1: "b" }, original) - end - - test "transform_keys returns a Hash instance when self is inherited from Hash" do - class HashDescendant < ::Hash - def initialize(elements = nil) - super(elements) - (elements || {}).each_pair { |key, value| self[key] = value } - end - end - - original = HashDescendant.new(a: "a", b: "b") - mapped = original.transform_keys { |k| "#{k}!".to_sym } - - assert_equal({ a: "a", b: "b" }, original) - assert_equal({ a!: "a", b!: "b" }, mapped) - assert_equal(::Hash, mapped.class) - end -end diff --git a/activesupport/test/core_ext/hash/transform_values_test.rb b/activesupport/test/core_ext/hash/transform_values_test.rb index d34b7fa7b9..e481b5e4a9 100644 --- a/activesupport/test/core_ext/hash/transform_values_test.rb +++ b/activesupport/test/core_ext/hash/transform_values_test.rb @@ -2,25 +2,16 @@ require "abstract_unit" require "active_support/core_ext/hash/indifferent_access" -require "active_support/core_ext/hash/transform_values" -class TransformValuesTest < ActiveSupport::TestCase - test "transform_values returns a new hash with the values computed from the block" do - original = { a: "a", b: "b" } - mapped = original.transform_values { |v| v + "!" } - - assert_equal({ a: "a", b: "b" }, original) - assert_equal({ a: "a!", b: "b!" }, mapped) - end - - test "transform_values! modifies the values of the original" do - original = { a: "a", b: "b" } - mapped = original.transform_values! { |v| v + "!" } - - assert_equal({ a: "a!", b: "b!" }, original) - assert_same original, mapped +class TransformValuesDeprecatedRequireTest < ActiveSupport::TestCase + test "requiring transform_values is deprecated" do + assert_deprecated do + require "active_support/core_ext/hash/transform_values" + end end +end +class IndifferentTransformValuesTest < ActiveSupport::TestCase test "indifferent access is still indifferent after mapping values" do original = { a: "a", b: "b" }.with_indifferent_access mapped = original.transform_values { |v| v + "!" } @@ -28,50 +19,4 @@ class TransformValuesTest < ActiveSupport::TestCase assert_equal "a!", mapped[:a] assert_equal "a!", mapped["a"] end - - # This is to be consistent with the behavior of Ruby's built in methods - # (e.g. #select, #reject) as of 2.2 - test "default values do not persist during mapping" do - original = Hash.new("foo") - original[:a] = "a" - mapped = original.transform_values { |v| v + "!" } - - assert_equal "a!", mapped[:a] - assert_nil mapped[:b] - end - - test "default procs do not persist after mapping" do - original = Hash.new { "foo" } - original[:a] = "a" - mapped = original.transform_values { |v| v + "!" } - - assert_equal "a!", mapped[:a] - assert_nil mapped[:b] - end - - test "transform_values returns a sized Enumerator if no block is given" do - original = { a: "a", b: "b" } - enumerator = original.transform_values - assert_equal original.size, enumerator.size - assert_equal Enumerator, enumerator.class - end - - test "transform_values! returns a sized Enumerator if no block is given" do - original = { a: "a", b: "b" } - enumerator = original.transform_values! - assert_equal original.size, enumerator.size - assert_equal Enumerator, enumerator.class - end - - test "transform_values is chainable with Enumerable methods" do - original = { a: "a", b: "b" } - mapped = original.transform_values.with_index { |v, i| [v, i].join } - assert_equal({ a: "a0", b: "b1" }, mapped) - end - - test "transform_values! is chainable with Enumerable methods" do - original = { a: "a", b: "b" } - original.transform_values!.with_index { |v, i| [v, i].join } - assert_equal({ a: "a0", b: "b1" }, original) - end end diff --git a/activesupport/test/core_ext/hash_ext_test.rb b/activesupport/test/core_ext/hash_ext_test.rb index 746d7ad416..8572d56722 100644 --- a/activesupport/test/core_ext/hash_ext_test.rb +++ b/activesupport/test/core_ext/hash_ext_test.rb @@ -31,10 +31,10 @@ class HashExtTest < ActiveSupport::TestCase def test_methods h = {} - assert_respond_to h, :transform_keys - assert_respond_to h, :transform_keys! 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 @@ -45,24 +45,10 @@ class HashExtTest < ActiveSupport::TestCase assert_respond_to h, :deep_stringify_keys! assert_respond_to h, :to_options assert_respond_to h, :to_options! - assert_respond_to h, :compact - assert_respond_to h, :compact! assert_respond_to h, :except assert_respond_to h, :except! end - def test_transform_keys - assert_equal @upcase_strings, @strings.transform_keys { |key| key.to_s.upcase } - assert_equal @upcase_strings, @symbols.transform_keys { |key| key.to_s.upcase } - assert_equal @upcase_strings, @mixed.transform_keys { |key| key.to_s.upcase } - end - - def test_transform_keys_not_mutates - transformed_hash = @mixed.dup - transformed_hash.transform_keys { |key| key.to_s.upcase } - assert_equal @mixed, transformed_hash - end - def test_deep_transform_keys assert_equal @nested_upcase_strings, @nested_symbols.deep_transform_keys { |key| key.to_s.upcase } assert_equal @nested_upcase_strings, @nested_strings.deep_transform_keys { |key| key.to_s.upcase } @@ -78,19 +64,6 @@ class HashExtTest < ActiveSupport::TestCase assert_equal @nested_mixed, transformed_hash end - def test_transform_keys! - assert_equal @upcase_strings, @symbols.dup.transform_keys! { |key| key.to_s.upcase } - assert_equal @upcase_strings, @strings.dup.transform_keys! { |key| key.to_s.upcase } - assert_equal @upcase_strings, @mixed.dup.transform_keys! { |key| key.to_s.upcase } - end - - def test_transform_keys_with_bang_mutates - transformed_hash = @mixed.dup - transformed_hash.transform_keys! { |key| key.to_s.upcase } - assert_equal @upcase_strings, transformed_hash - assert_equal({ :a => 1, "b" => 2 }, @mixed) - end - def test_deep_transform_keys! assert_equal @nested_upcase_strings, @nested_symbols.deep_dup.deep_transform_keys! { |key| key.to_s.upcase } assert_equal @nested_upcase_strings, @nested_strings.deep_dup.deep_transform_keys! { |key| key.to_s.upcase } @@ -107,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 @@ -339,30 +337,16 @@ class HashExtTest < ActiveSupport::TestCase assert_equal expected, merged end - def test_slice - original = { a: "x", b: "y", c: 10 } - expected = { a: "x", b: "y" } - - # Should return a new hash with only the given keys. - assert_equal expected, original.slice(:a, :b) - assert_not_equal expected, original - end - def test_slice_inplace original = { a: "x", b: "y", c: 10 } - expected = { c: 10 } + expected_return = { c: 10 } + expected_original = { a: "x", b: "y" } - # Should replace the hash with only the given keys. - assert_equal expected, original.slice!(:a, :b) - end + # Should return a hash containing the removed key/value pairs. + assert_equal expected_return, original.slice!(:a, :b) - def test_slice_with_an_array_key - original = { :a => "x", :b => "y", :c => 10, [:a, :b] => "an array key" } - expected = { [:a, :b] => "an array key", :c => 10 } - - # Should return a new hash with only the given keys when given an array key. - assert_equal expected, original.slice([:a, :b], :c) - assert_not_equal expected, original + # Should replace the hash with only the given keys. + assert_equal expected_original, original end def test_slice_inplace_with_an_array_key @@ -373,14 +357,6 @@ class HashExtTest < ActiveSupport::TestCase assert_equal expected, original.slice!([:a, :b], :c) end - def test_slice_with_splatted_keys - original = { :a => "x", :b => "y", :c => 10, [:a, :b] => "an array key" } - expected = { a: "x", b: "y" } - - # Should grab each of the splatted keys. - assert_equal expected, original.slice(*[:a, :b]) - end - def test_slice_bang_does_not_override_default hash = Hash.new(0) hash.update(a: 1, b: 2) @@ -446,7 +422,7 @@ class HashExtTest < ActiveSupport::TestCase original.freeze assert_nothing_raised { original.except(:a) } - assert_raise(RuntimeError) { original.except!(:a) } + assert_raise(FrozenError) { original.except!(:a) } end def test_except_does_not_delete_values_in_original @@ -456,38 +432,10 @@ class HashExtTest < ActiveSupport::TestCase end end - def test_compact - hash_contain_nil_value = @symbols.merge(z: nil) - hash_with_only_nil_values = { a: nil, b: nil } - - h = hash_contain_nil_value.dup - assert_equal(@symbols, h.compact) - assert_equal(hash_contain_nil_value, h) - - h = hash_with_only_nil_values.dup - assert_equal({}, h.compact) - assert_equal(hash_with_only_nil_values, h) - - h = @symbols.dup - assert_equal(@symbols, h.compact) - assert_equal(@symbols, h) - end - - def test_compact! - hash_contain_nil_value = @symbols.merge(z: nil) - hash_with_only_nil_values = { a: nil, b: nil } - - h = hash_contain_nil_value.dup - assert_equal(@symbols, h.compact!) - assert_equal(@symbols, h) - - h = hash_with_only_nil_values.dup - assert_equal({}, h.compact!) - assert_equal({}, h) - - h = @symbols.dup - assert_nil(h.compact!) - assert_equal(@symbols, h) + def test_requiring_compact_is_deprecated + assert_deprecated do + require "active_support/core_ext/hash/compact" + end end end @@ -1061,7 +1009,7 @@ class HashToXmlTest < ActiveSupport::TestCase </alert> XML alert_at = Hash.from_xml(alert_xml)["alert"]["alert_at"] - assert alert_at.utc? + assert_predicate alert_at, :utc? assert_equal Time.utc(2008, 2, 10, 15, 30, 45), alert_at end @@ -1072,7 +1020,7 @@ class HashToXmlTest < ActiveSupport::TestCase </alert> XML alert_at = Hash.from_xml(alert_xml)["alert"]["alert_at"] - assert alert_at.utc? + assert_predicate alert_at, :utc? assert_equal Time.utc(2008, 2, 10, 15, 30, 45), alert_at end @@ -1083,7 +1031,7 @@ class HashToXmlTest < ActiveSupport::TestCase </alert> XML alert_at = Hash.from_xml(alert_xml)["alert"]["alert_at"] - assert alert_at.utc? + assert_predicate alert_at, :utc? assert_equal 2050, alert_at.year assert_equal 2, alert_at.month assert_equal 10, alert_at.day diff --git a/activesupport/test/core_ext/integer_ext_test.rb b/activesupport/test/core_ext/integer_ext_test.rb index 14169b084d..5691dc5341 100644 --- a/activesupport/test/core_ext/integer_ext_test.rb +++ b/activesupport/test/core_ext/integer_ext_test.rb @@ -8,14 +8,14 @@ class IntegerExtTest < ActiveSupport::TestCase def test_multiple_of [ -7, 0, 7, 14 ].each { |i| assert i.multiple_of?(7) } - [ -7, 7, 14 ].each { |i| assert ! i.multiple_of?(6) } + [ -7, 7, 14 ].each { |i| assert_not i.multiple_of?(6) } # test the 0 edge case assert 0.multiple_of?(0) - assert !5.multiple_of?(0) + assert_not 5.multiple_of?(0) # test with a prime - [2, 3, 5, 7].each { |i| assert !PRIME.multiple_of?(i) } + [2, 3, 5, 7].each { |i| assert_not PRIME.multiple_of?(i) } end def test_ordinalize diff --git a/activesupport/test/core_ext/load_error_test.rb b/activesupport/test/core_ext/load_error_test.rb index 41b11d0c33..6d3726e407 100644 --- a/activesupport/test/core_ext/load_error_test.rb +++ b/activesupport/test/core_ext/load_error_test.rb @@ -7,13 +7,19 @@ class TestLoadError < ActiveSupport::TestCase def test_with_require assert_raise(LoadError) { require "no_this_file_don't_exist" } end + def test_with_load assert_raise(LoadError) { load "nor_does_this_one" } end + def test_path - begin load "nor/this/one.rb" - rescue LoadError => e - assert_equal "nor/this/one.rb", e.path - end + load "nor/this/one.rb" + rescue LoadError => e + assert_equal "nor/this/one.rb", e.path + end + + def test_is_missing_with_nil_path + error = LoadError.new(nil) + assert_nothing_raised { error.is_missing?("anything") } end end diff --git a/activesupport/test/core_ext/module/anonymous_test.rb b/activesupport/test/core_ext/module/anonymous_test.rb index 606f22c9b5..e03c217015 100644 --- a/activesupport/test/core_ext/module/anonymous_test.rb +++ b/activesupport/test/core_ext/module/anonymous_test.rb @@ -5,12 +5,12 @@ require "active_support/core_ext/module/anonymous" class AnonymousTest < ActiveSupport::TestCase test "an anonymous class or module are anonymous" do - assert Module.new.anonymous? - assert Class.new.anonymous? + assert_predicate Module.new, :anonymous? + assert_predicate Class.new, :anonymous? end test "a named class or module are not anonymous" do - assert !Kernel.anonymous? - assert !Object.anonymous? + assert_not_predicate Kernel, :anonymous? + assert_not_predicate Object, :anonymous? end end diff --git a/activesupport/test/core_ext/module/attr_internal_test.rb b/activesupport/test/core_ext/module/attr_internal_test.rb index c2a28eced4..9a65f75497 100644 --- a/activesupport/test/core_ext/module/attr_internal_test.rb +++ b/activesupport/test/core_ext/module/attr_internal_test.rb @@ -12,7 +12,7 @@ class AttrInternalTest < ActiveSupport::TestCase def test_reader assert_nothing_raised { @target.attr_internal_reader :foo } - assert !@instance.instance_variable_defined?("@_foo") + assert_not @instance.instance_variable_defined?("@_foo") assert_raise(NoMethodError) { @instance.foo = 1 } @instance.instance_variable_set("@_foo", 1) @@ -22,7 +22,7 @@ class AttrInternalTest < ActiveSupport::TestCase def test_writer assert_nothing_raised { @target.attr_internal_writer :foo } - assert !@instance.instance_variable_defined?("@_foo") + assert_not @instance.instance_variable_defined?("@_foo") assert_nothing_raised { assert_equal 1, @instance.foo = 1 } assert_equal 1, @instance.instance_variable_get("@_foo") @@ -32,7 +32,7 @@ class AttrInternalTest < ActiveSupport::TestCase def test_accessor assert_nothing_raised { @target.attr_internal :foo } - assert !@instance.instance_variable_defined?("@_foo") + assert_not @instance.instance_variable_defined?("@_foo") assert_nothing_raised { assert_equal 1, @instance.foo = 1 } assert_equal 1, @instance.instance_variable_get("@_foo") @@ -44,10 +44,10 @@ class AttrInternalTest < ActiveSupport::TestCase assert_nothing_raised { Module.attr_internal_naming_format = "@abc%sdef" } @target.attr_internal :foo - assert !@instance.instance_variable_defined?("@_foo") - assert !@instance.instance_variable_defined?("@abcfoodef") + assert_not @instance.instance_variable_defined?("@_foo") + assert_not @instance.instance_variable_defined?("@abcfoodef") assert_nothing_raised { @instance.foo = 1 } - assert !@instance.instance_variable_defined?("@_foo") + assert_not @instance.instance_variable_defined?("@_foo") assert @instance.instance_variable_defined?("@abcfoodef") ensure Module.attr_internal_naming_format = "@_%s" diff --git a/activesupport/test/core_ext/module/attribute_accessor_per_thread_test.rb b/activesupport/test/core_ext/module/attribute_accessor_per_thread_test.rb index e0fbd1002c..e0e331fc91 100644 --- a/activesupport/test/core_ext/module/attribute_accessor_per_thread_test.rb +++ b/activesupport/test/core_ext/module/attribute_accessor_per_thread_test.rb @@ -43,22 +43,22 @@ class ModuleAttributeAccessorPerThreadTest < ActiveSupport::TestCase assert_respond_to @class, :foo assert_respond_to @class, :foo= assert_respond_to @object, :bar - assert !@object.respond_to?(:bar=) + assert_not_respond_to @object, :bar= end.join end def test_should_not_create_instance_reader Thread.new do assert_respond_to @class, :shaq - assert !@object.respond_to?(:shaq) + assert_not_respond_to @object, :shaq end.join end def test_should_not_create_instance_accessors Thread.new do assert_respond_to @class, :camp - assert !@object.respond_to?(:camp) - assert !@object.respond_to?(:camp=) + assert_not_respond_to @object, :camp + assert_not_respond_to @object, :camp= end.join end diff --git a/activesupport/test/core_ext/module/attribute_accessor_test.rb b/activesupport/test/core_ext/module/attribute_accessor_test.rb index f1d6859a88..33c583947a 100644 --- a/activesupport/test/core_ext/module/attribute_accessor_test.rb +++ b/activesupport/test/core_ext/module/attribute_accessor_test.rb @@ -65,18 +65,18 @@ class ModuleAttributeAccessorTest < ActiveSupport::TestCase assert_respond_to @module, :foo assert_respond_to @module, :foo= assert_respond_to @object, :bar - assert !@object.respond_to?(:bar=) + assert_not_respond_to @object, :bar= end def test_should_not_create_instance_reader assert_respond_to @module, :shaq - assert !@object.respond_to?(:shaq) + assert_not_respond_to @object, :shaq end def test_should_not_create_instance_accessors assert_respond_to @module, :camp - assert !@object.respond_to?(:camp) - assert !@object.respond_to?(:camp=) + assert_not_respond_to @object, :camp + assert_not_respond_to @object, :camp= end def test_should_raise_name_error_if_attribute_name_is_invalid diff --git a/activesupport/test/core_ext/module/attribute_aliasing_test.rb b/activesupport/test/core_ext/module/attribute_aliasing_test.rb index 187a0f4da2..81aac224f9 100644 --- a/activesupport/test/core_ext/module/attribute_aliasing_test.rb +++ b/activesupport/test/core_ext/module/attribute_aliasing_test.rb @@ -30,15 +30,15 @@ class AttributeAliasingTest < ActiveSupport::TestCase def test_attribute_alias e = AttributeAliasing::Email.new - assert !e.subject? + assert_not_predicate e, :subject? e.title = "Upgrade computer" assert_equal "Upgrade computer", e.subject - assert e.subject? + assert_predicate e, :subject? e.subject = "We got a long way to go" assert_equal "We got a long way to go", e.title - assert e.title? + assert_predicate e, :title? end def test_aliasing_to_uppercase_attributes @@ -47,15 +47,15 @@ class AttributeAliasingTest < ActiveSupport::TestCase # to more sensible ones, everything goes *foof*. e = AttributeAliasing::Email.new - assert !e.body? - assert !e.Data? + assert_not_predicate e, :body? + assert_not_predicate e, :Data? e.body = "No, really, this is not a joke." assert_equal "No, really, this is not a joke.", e.Data - assert e.Data? + assert_predicate e, :Data? e.Data = "Uppercased methods are the suck" assert_equal "Uppercased methods are the suck", e.body - assert e.body? + assert_predicate e, :body? end end diff --git a/activesupport/test/core_ext/module/concerning_test.rb b/activesupport/test/core_ext/module/concerning_test.rb index 192c3d5a9c..38fd60463d 100644 --- a/activesupport/test/core_ext/module/concerning_test.rb +++ b/activesupport/test/core_ext/module/concerning_test.rb @@ -5,7 +5,7 @@ require "active_support/core_ext/module/concerning" class ModuleConcerningTest < ActiveSupport::TestCase def test_concerning_declares_a_concern_and_includes_it_immediately - klass = Class.new { concerning(:Foo) {} } + klass = Class.new { concerning(:Foo) { } } assert_includes klass.ancestors, klass::Foo, klass.ancestors.inspect end end @@ -21,7 +21,7 @@ class ModuleConcernTest < ActiveSupport::TestCase # Declares a concern but doesn't include it assert klass.const_defined?(:Baz, false) - assert !ModuleConcernTest.const_defined?(:Baz) + assert_not ModuleConcernTest.const_defined?(:Baz) assert_kind_of ActiveSupport::Concern, klass::Baz assert_not_includes klass.ancestors, klass::Baz, klass.ancestors.inspect @@ -55,10 +55,10 @@ class ModuleConcernTest < ActiveSupport::TestCase end def test_using_class_methods_blocks_instead_of_ClassMethods_module - assert !Foo.respond_to?(:will_be_orphaned) - assert Foo.respond_to?(:hacked_on) - assert Foo.respond_to?(:nicer_dsl) - assert Foo.respond_to?(:doesnt_clobber) + assert_not_respond_to Foo, :will_be_orphaned + assert_respond_to Foo, :hacked_on + assert_respond_to Foo, :nicer_dsl + assert_respond_to Foo, :doesnt_clobber # Orphan in Foo::ClassMethods, not Bar::ClassMethods. assert Foo.const_defined?(:ClassMethods) diff --git a/activesupport/test/core_ext/module/introspection_test.rb b/activesupport/test/core_ext/module/introspection_test.rb index 76d3012239..d8409d5e44 100644 --- a/activesupport/test/core_ext/module/introspection_test.rb +++ b/activesupport/test/core_ext/module/introspection_test.rb @@ -15,25 +15,43 @@ module ParentA end class IntrospectionTest < ActiveSupport::TestCase + def test_module_parent_name + assert_equal "ParentA", ParentA::B.module_parent_name + assert_equal "ParentA::B", ParentA::B::C.module_parent_name + assert_nil ParentA.module_parent_name + end + + def test_module_parent_name_when_frozen + assert_equal "ParentA", ParentA::FrozenB.module_parent_name + assert_equal "ParentA::B", ParentA::B::FrozenC.module_parent_name + end + def test_parent_name - assert_equal "ParentA", ParentA::B.parent_name - assert_equal "ParentA::B", ParentA::B::C.parent_name - assert_nil ParentA.parent_name + assert_deprecated do + assert_equal "ParentA", ParentA::B.parent_name + end end - def test_parent_name_when_frozen - assert_equal "ParentA", ParentA::FrozenB.parent_name - assert_equal "ParentA::B", ParentA::B::FrozenC.parent_name + def test_module_parent + assert_equal ParentA::B, ParentA::B::C.module_parent + assert_equal ParentA, ParentA::B.module_parent + assert_equal Object, ParentA.module_parent end def test_parent - assert_equal ParentA::B, ParentA::B::C.parent - assert_equal ParentA, ParentA::B.parent - assert_equal Object, ParentA.parent + assert_deprecated do + assert_equal ParentA, ParentA::B.parent + end + end + + def test_module_parents + assert_equal [ParentA::B, ParentA, Object], ParentA::B::C.module_parents + assert_equal [ParentA, Object], ParentA::B.module_parents end def test_parents - assert_equal [ParentA::B, ParentA, Object], ParentA::B::C.parents - assert_equal [ParentA, Object], ParentA::B.parents + assert_deprecated do + assert_equal [ParentA, Object], ParentA::B.parents + end end end diff --git a/activesupport/test/core_ext/module/reachable_test.rb b/activesupport/test/core_ext/module/reachable_test.rb deleted file mode 100644 index 097a72fa5b..0000000000 --- a/activesupport/test/core_ext/module/reachable_test.rb +++ /dev/null @@ -1,51 +0,0 @@ -# frozen_string_literal: true - -require "abstract_unit" -require "active_support/core_ext/module/reachable" - -class AnonymousTest < ActiveSupport::TestCase - test "an anonymous class or module is not reachable" do - assert_deprecated do - assert !Module.new.reachable? - assert !Class.new.reachable? - end - end - - test "ordinary named classes or modules are reachable" do - assert_deprecated do - assert Kernel.reachable? - assert Object.reachable? - end - end - - test "a named class or module whose constant has gone is not reachable" do - c = eval "class C; end; C" - m = eval "module M; end; M" - - self.class.send(:remove_const, :C) - self.class.send(:remove_const, :M) - - assert_deprecated do - assert !c.reachable? - assert !m.reachable? - end - end - - test "a named class or module whose constants store different objects are not reachable" do - c = eval "class C; end; C" - m = eval "module M; end; M" - - self.class.send(:remove_const, :C) - self.class.send(:remove_const, :M) - - eval "class C; end" - eval "module M; end" - - assert_deprecated do - assert C.reachable? - assert M.reachable? - assert !c.reachable? - assert !m.reachable? - end - end -end diff --git a/activesupport/test/core_ext/module/remove_method_test.rb b/activesupport/test/core_ext/module/remove_method_test.rb index 8493be8d08..a18fc0a5e4 100644 --- a/activesupport/test/core_ext/module/remove_method_test.rb +++ b/activesupport/test/core_ext/module/remove_method_test.rb @@ -32,14 +32,14 @@ class RemoveMethodTest < ActiveSupport::TestCase RemoveMethodTests::A.class_eval { remove_possible_method(:do_something) } - assert !RemoveMethodTests::A.new.respond_to?(:do_something) + assert_not_respond_to RemoveMethodTests::A.new, :do_something end def test_remove_singleton_method_from_an_object RemoveMethodTests::A.class_eval { remove_possible_singleton_method(:do_something_else) } - assert !RemoveMethodTests::A.respond_to?(:do_something_else) + assert_not_respond_to RemoveMethodTests::A, :do_something_else end def test_redefine_method_in_an_object diff --git a/activesupport/test/core_ext/module_test.rb b/activesupport/test/core_ext/module_test.rb index e918823074..04692f1484 100644 --- a/activesupport/test/core_ext/module_test.rb +++ b/activesupport/test/core_ext/module_test.rb @@ -347,7 +347,7 @@ class ModuleTest < ActiveSupport::TestCase def test_delegation_with_method_arguments has_block = HasBlock.new(Block.new) - assert has_block.hello? + assert_predicate has_block, :hello? end def test_delegate_missing_to_with_method @@ -383,9 +383,9 @@ class ModuleTest < ActiveSupport::TestCase 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_respond_to DecoratedTester.new(@david), :name + assert_not_respond_to DecoratedTester.new(@david), :private_name + assert_not_respond_to DecoratedTester.new(@david), :my_fake_method assert DecoratedTester.new(@david).respond_to?(:name, true) assert_not DecoratedTester.new(@david).respond_to?(:private_name, true) @@ -414,8 +414,8 @@ class ModuleTest < ActiveSupport::TestCase place = location.new(Somewhere.new("Such street", "Sad city")) - assert_not place.respond_to?(:street) - assert_not place.respond_to?(:city) + assert_not_respond_to place, :street + assert_not_respond_to place, :city assert place.respond_to?(:street, true) # Asking for private method assert place.respond_to?(:city, true) @@ -432,12 +432,79 @@ class ModuleTest < ActiveSupport::TestCase place = location.new(Somewhere.new("Such street", "Sad city")) - assert_not place.respond_to?(:street) - assert_not place.respond_to?(:city) + assert_not_respond_to place, :street + assert_not_respond_to place, :city - assert_not place.respond_to?(:the_street) + assert_not_respond_to place, :the_street assert place.respond_to?(:the_street, true) - assert_not place.respond_to?(:the_city) + assert_not_respond_to place, :the_city assert place.respond_to?(:the_city, true) end + + def test_private_delegate_with_private_option + location = Class.new do + def initialize(place) + @place = place + end + + delegate(:street, :city, to: :@place, private: true) + end + + place = location.new(Somewhere.new("Such street", "Sad city")) + + assert_not_respond_to place, :street + assert_not_respond_to place, :city + + assert place.respond_to?(:street, true) # Asking for private method + assert place.respond_to?(:city, true) + end + + def test_some_public_some_private_delegate_with_private_option + location = Class.new do + def initialize(place) + @place = place + end + + delegate(:street, to: :@place) + delegate(:city, to: :@place, private: true) + end + + place = location.new(Somewhere.new("Such street", "Sad city")) + + assert_respond_to place, :street + assert_not_respond_to place, :city + + assert place.respond_to?(:city, true) # Asking for private method + end + + def test_private_delegate_prefixed_with_private_option + location = Class.new do + def initialize(place) + @place = place + end + + delegate(:street, :city, to: :@place, prefix: :the, private: true) + end + + place = location.new(Somewhere.new("Such street", "Sad city")) + + assert_not_respond_to place, :the_street + assert place.respond_to?(:the_street, true) + assert_not_respond_to place, :the_city + assert place.respond_to?(:the_city, true) + end + + def test_delegate_with_private_option_returns_names_of_delegate_methods + location = Class.new do + def initialize(place) + @place = place + end + end + + assert_equal [:street, :city], + location.delegate(:street, :city, to: :@place, private: true) + + assert_equal [:the_street, :the_city], + location.delegate(:street, :city, to: :@place, prefix: :the, private: true) + end end diff --git a/activesupport/test/core_ext/name_error_test.rb b/activesupport/test/core_ext/name_error_test.rb index d1dace3713..5c6c12ffc7 100644 --- a/activesupport/test/core_ext/name_error_test.rb +++ b/activesupport/test/core_ext/name_error_test.rb @@ -17,7 +17,7 @@ class NameErrorTest < ActiveSupport::TestCase exc = assert_raise NameError do some_method_that_does_not_exist end - assert !exc.missing_name?(:Foo) + assert_not exc.missing_name?(:Foo) assert_nil exc.missing_name end end diff --git a/activesupport/test/core_ext/numeric_ext_test.rb b/activesupport/test/core_ext/numeric_ext_test.rb index 4b9073da54..5005b9febd 100644 --- a/activesupport/test/core_ext/numeric_ext_test.rb +++ b/activesupport/test/core_ext/numeric_ext_test.rb @@ -406,86 +406,9 @@ class NumericExtFormattingTest < ActiveSupport::TestCase assert_equal 10_000, 10.seconds.in_milliseconds end - # TODO: Remove positive and negative tests when we drop support to ruby < 2.3 - b = 2**64 - - T_ZERO = b.coerce(0).first - T_ONE = b.coerce(1).first - T_MONE = b.coerce(-1).first - - def test_positive - assert_predicate(1, :positive?) - assert_not_predicate(0, :positive?) - assert_not_predicate(-1, :positive?) - assert_predicate(+1.0, :positive?) - assert_not_predicate(+0.0, :positive?) - assert_not_predicate(-0.0, :positive?) - assert_not_predicate(-1.0, :positive?) - assert_predicate(+(0.0.next_float), :positive?) - assert_not_predicate(-(0.0.next_float), :positive?) - assert_predicate(Float::INFINITY, :positive?) - assert_not_predicate(-Float::INFINITY, :positive?) - assert_not_predicate(Float::NAN, :positive?) - - a = Class.new(Numeric) do - def >(x); true; end - end.new - assert_predicate(a, :positive?) - - a = Class.new(Numeric) do - def >(x); false; end - end.new - assert_not_predicate(a, :positive?) - - assert_predicate(1 / 2r, :positive?) - assert_not_predicate(-1 / 2r, :positive?) - - assert_predicate(T_ONE, :positive?) - assert_not_predicate(T_MONE, :positive?) - assert_not_predicate(T_ZERO, :positive?) - - e = assert_raises(NoMethodError) do - Complex(1).positive? + def test_requiring_inquiry_is_deprecated + assert_deprecated do + require "active_support/core_ext/numeric/inquiry" end - - assert_match(/positive\?/, e.message) - end - - def test_negative - assert_predicate(-1, :negative?) - assert_not_predicate(0, :negative?) - assert_not_predicate(1, :negative?) - assert_predicate(-1.0, :negative?) - assert_not_predicate(-0.0, :negative?) - assert_not_predicate(+0.0, :negative?) - assert_not_predicate(+1.0, :negative?) - assert_predicate(-(0.0.next_float), :negative?) - assert_not_predicate(+(0.0.next_float), :negative?) - assert_predicate(-Float::INFINITY, :negative?) - assert_not_predicate(Float::INFINITY, :negative?) - assert_not_predicate(Float::NAN, :negative?) - - a = Class.new(Numeric) do - def <(x); true; end - end.new - assert_predicate(a, :negative?) - - a = Class.new(Numeric) do - def <(x); false; end - end.new - assert_not_predicate(a, :negative?) - - assert_predicate(-1 / 2r, :negative?) - assert_not_predicate(1 / 2r, :negative?) - - assert_not_predicate(T_ONE, :negative?) - assert_predicate(T_MONE, :negative?) - assert_not_predicate(T_ZERO, :negative?) - - e = assert_raises(NoMethodError) do - Complex(1).negative? - end - - assert_match(/negative\?/, e.message) end end diff --git a/activesupport/test/core_ext/object/acts_like_test.rb b/activesupport/test/core_ext/object/acts_like_test.rb index 9f7b81f7fc..31241caf0a 100644 --- a/activesupport/test/core_ext/object/acts_like_test.rb +++ b/activesupport/test/core_ext/object/acts_like_test.rb @@ -17,19 +17,19 @@ class ObjectTests < ActiveSupport::TestCase dt = DateTime.new duck = DuckTime.new - assert !object.acts_like?(:time) - assert !object.acts_like?(:date) + assert_not object.acts_like?(:time) + assert_not object.acts_like?(:date) assert time.acts_like?(:time) - assert !time.acts_like?(:date) + assert_not time.acts_like?(:date) - assert !date.acts_like?(:time) + assert_not date.acts_like?(:time) assert date.acts_like?(:date) assert dt.acts_like?(:time) assert dt.acts_like?(:date) assert duck.acts_like?(:time) - assert !duck.acts_like?(:date) + assert_not duck.acts_like?(:date) end end diff --git a/activesupport/test/core_ext/object/blank_test.rb b/activesupport/test/core_ext/object/blank_test.rb index 749e59ec00..954f415383 100644 --- a/activesupport/test/core_ext/object/blank_test.rb +++ b/activesupport/test/core_ext/object/blank_test.rb @@ -16,8 +16,8 @@ class BlankTest < ActiveSupport::TestCase end end - BLANK = [ EmptyTrue.new, nil, false, "", " ", " \n\t \r ", " ", "\u00a0", [], {} ] - NOT = [ EmptyFalse.new, Object.new, true, 0, 1, "a", [nil], { nil => 0 }, Time.now ] + BLANK = [ EmptyTrue.new, nil, false, "", " ", " \n\t \r ", " ", "\u00a0", [], {}, " ".encode("UTF-16LE") ] + NOT = [ EmptyFalse.new, Object.new, true, 0, 1, "a", [nil], { nil => 0 }, Time.now, "my value".encode("UTF-16LE") ] def test_blank BLANK.each { |v| assert_equal true, v.blank?, "#{v.inspect} should be blank" } diff --git a/activesupport/test/core_ext/object/deep_dup_test.rb b/activesupport/test/core_ext/object/deep_dup_test.rb index 2486592441..1fb26ebac7 100644 --- a/activesupport/test/core_ext/object/deep_dup_test.rb +++ b/activesupport/test/core_ext/object/deep_dup_test.rb @@ -47,7 +47,7 @@ class DeepDupTest < ActiveSupport::TestCase object = Object.new dup = object.deep_dup dup.instance_variable_set(:@a, 1) - assert !object.instance_variable_defined?(:@a) + assert_not object.instance_variable_defined?(:@a) assert dup.instance_variable_defined?(:@a) end diff --git a/activesupport/test/core_ext/object/duplicable_test.rb b/activesupport/test/core_ext/object/duplicable_test.rb index b984becce3..5203434ae6 100644 --- a/activesupport/test/core_ext/object/duplicable_test.rb +++ b/activesupport/test/core_ext/object/duplicable_test.rb @@ -9,15 +9,9 @@ class DuplicableTest < ActiveSupport::TestCase if RUBY_VERSION >= "2.5.0" RAISE_DUP = [method(:puts)] ALLOW_DUP = ["1", "symbol_from_string".to_sym, Object.new, /foo/, [], {}, Time.now, Class.new, Module.new, BigDecimal("4.56"), nil, false, true, 1, 2.3, Complex(1), Rational(1)] - elsif RUBY_VERSION >= "2.4.1" + else RAISE_DUP = [method(:puts), Complex(1), Rational(1)] ALLOW_DUP = ["1", "symbol_from_string".to_sym, Object.new, /foo/, [], {}, Time.now, Class.new, Module.new, BigDecimal("4.56"), nil, false, true, 1, 2.3] - elsif RUBY_VERSION >= "2.4.0" # Due to 2.4.0 bug. This elsif cannot be removed unless we drop 2.4.0 support... - RAISE_DUP = [method(:puts), Complex(1), Rational(1), "symbol_from_string".to_sym] - ALLOW_DUP = ["1", Object.new, /foo/, [], {}, Time.now, Class.new, Module.new, BigDecimal("4.56"), nil, false, true, 1, 2.3] - else - RAISE_DUP = [nil, false, true, :symbol, 1, 2.3, method(:puts), Complex(1), Rational(1)] - ALLOW_DUP = ["1", Object.new, /foo/, [], {}, Time.now, Class.new, Module.new, BigDecimal("4.56")] end def test_duplicable @@ -25,7 +19,7 @@ class DuplicableTest < ActiveSupport::TestCase "* https://github.com/rubinius/rubinius/issues/3089" RAISE_DUP.each do |v| - assert !v.duplicable?, "#{ v.inspect } should not be duplicable" + assert_not v.duplicable?, "#{ v.inspect } should not be duplicable" assert_raises(TypeError, v.class.name) { v.dup } end diff --git a/activesupport/test/core_ext/object/inclusion_test.rb b/activesupport/test/core_ext/object/inclusion_test.rb index 52c21f2e8e..8cbb4f848f 100644 --- a/activesupport/test/core_ext/object/inclusion_test.rb +++ b/activesupport/test/core_ext/object/inclusion_test.rb @@ -6,30 +6,30 @@ require "active_support/core_ext/object/inclusion" class InTest < ActiveSupport::TestCase def test_in_array assert 1.in?([1, 2]) - assert !3.in?([1, 2]) + assert_not 3.in?([1, 2]) end def test_in_hash h = { "a" => 100, "b" => 200 } assert "a".in?(h) - assert !"z".in?(h) + assert_not "z".in?(h) end def test_in_string assert "lo".in?("hello") - assert !"ol".in?("hello") + assert_not "ol".in?("hello") assert ?h.in?("hello") end def test_in_range assert 25.in?(1..50) - assert !75.in?(1..50) + assert_not 75.in?(1..50) end def test_in_set s = Set.new([1, 2]) assert 1.in?(s) - assert !3.in?(s) + assert_not 3.in?(s) end module A @@ -45,8 +45,8 @@ class InTest < ActiveSupport::TestCase def test_in_module assert A.in?(B) assert A.in?(C) - assert !A.in?(A) - assert !A.in?(D) + assert_not A.in?(A) + assert_not A.in?(D) end def test_no_method_catching diff --git a/activesupport/test/core_ext/object/instance_variables_test.rb b/activesupport/test/core_ext/object/instance_variables_test.rb index a3d8daab5b..9052d209a3 100644 --- a/activesupport/test/core_ext/object/instance_variables_test.rb +++ b/activesupport/test/core_ext/object/instance_variables_test.rb @@ -19,15 +19,15 @@ class ObjectInstanceVariableTest < ActiveSupport::TestCase end def test_instance_exec_passes_arguments_to_block - assert_equal %w(hello goodbye), "hello".dup.instance_exec("goodbye") { |v| [self, v] } + assert_equal %w(hello goodbye), (+"hello").instance_exec("goodbye") { |v| [self, v] } end def test_instance_exec_with_frozen_obj - assert_equal %w(olleh goodbye), "hello".freeze.instance_exec("goodbye") { |v| [reverse, v] } + assert_equal %w(olleh goodbye), "hello".instance_exec("goodbye") { |v| [reverse, v] } end def test_instance_exec_nested - assert_equal %w(goodbye olleh bar), "hello".dup.instance_exec("goodbye") { |arg| + assert_equal %w(goodbye olleh bar), (+"hello").instance_exec("goodbye") { |arg| [arg] + instance_exec("bar") { |v| [reverse, v] } } end end diff --git a/activesupport/test/core_ext/object/to_query_test.rb b/activesupport/test/core_ext/object/to_query_test.rb index 7593bcfa4d..561dadbbcf 100644 --- a/activesupport/test/core_ext/object/to_query_test.rb +++ b/activesupport/test/core_ext/object/to_query_test.rb @@ -77,6 +77,20 @@ class ToQueryTest < ActiveSupport::TestCase assert_equal "name=Nakshay&type=human", hash.to_query end + def test_hash_not_sorted_lexicographically_for_nested_structure + params = { + "foo" => { + "contents" => [ + { "name" => "gorby", "id" => "123" }, + { "name" => "puff", "d" => "true" } + ] + } + } + expected = "foo[contents][][name]=gorby&foo[contents][][id]=123&foo[contents][][name]=puff&foo[contents][][d]=true" + + assert_equal expected, URI.decode_www_form_component(params.to_query) + end + private def assert_query_equal(expected, actual) assert_equal expected.split("&"), actual.to_query.split("&") diff --git a/activesupport/test/core_ext/object/try_test.rb b/activesupport/test/core_ext/object/try_test.rb index fe68b24bf5..a838334034 100644 --- a/activesupport/test/core_ext/object/try_test.rb +++ b/activesupport/test/core_ext/object/try_test.rb @@ -10,25 +10,25 @@ class ObjectTryTest < ActiveSupport::TestCase def test_nonexisting_method method = :undefined_method - assert !@string.respond_to?(method) + assert_not_respond_to @string, method assert_nil @string.try(method) end def test_nonexisting_method_with_arguments method = :undefined_method - assert !@string.respond_to?(method) + assert_not_respond_to @string, method assert_nil @string.try(method, "llo", "y") end def test_nonexisting_method_bang method = :undefined_method - assert !@string.respond_to?(method) + assert_not_respond_to @string, method assert_raise(NoMethodError) { @string.try!(method) } end def test_nonexisting_method_with_arguments_bang method = :undefined_method - assert !@string.respond_to?(method) + assert_not_respond_to @string, method assert_raise(NoMethodError) { @string.try!(method, "llo", "y") } end @@ -78,7 +78,6 @@ class ObjectTryTest < ActiveSupport::TestCase def test_try_with_private_method_bang klass = Class.new do private - def private_method "private method" end @@ -90,7 +89,6 @@ class ObjectTryTest < ActiveSupport::TestCase def test_try_with_private_method klass = Class.new do private - def private_method "private method" end @@ -109,7 +107,6 @@ class ObjectTryTest < ActiveSupport::TestCase end private - def private_delegator_method "private delegator method" end @@ -120,11 +117,11 @@ class ObjectTryTest < ActiveSupport::TestCase end def test_try_with_method_on_delegator_target - assert_equal 5, Decorator.new(@string).size + assert_equal 5, Decorator.new(@string).try(:size) end def test_try_with_overridden_method_on_delegator - assert_equal "overridden reverse", Decorator.new(@string).reverse + assert_equal "overridden reverse", Decorator.new(@string).try(:reverse) end def test_try_with_private_method_on_delegator @@ -140,7 +137,6 @@ class ObjectTryTest < ActiveSupport::TestCase def test_try_with_private_method_on_delegator_target klass = Class.new do private - def private_method "private method" end @@ -152,7 +148,6 @@ class ObjectTryTest < ActiveSupport::TestCase def test_try_with_private_method_on_delegator_target_bang klass = Class.new do private - def private_method "private method" end diff --git a/activesupport/test/core_ext/range_ext_test.rb b/activesupport/test/core_ext/range_ext_test.rb index 903c173e59..4b8efb8a93 100644 --- a/activesupport/test/core_ext/range_ext_test.rb +++ b/activesupport/test/core_ext/range_ext_test.rb @@ -37,7 +37,7 @@ class RangeTest < ActiveSupport::TestCase end def test_overlaps_last_exclusive - assert !(1...5).overlaps?(5..10) + assert_not (1...5).overlaps?(5..10) end def test_overlaps_first_inclusive @@ -45,7 +45,7 @@ class RangeTest < ActiveSupport::TestCase end def test_overlaps_first_exclusive - assert !(5..10).overlaps?(1...5) + assert_not (5..10).overlaps?(1...5) end def test_should_include_identical_inclusive @@ -102,20 +102,20 @@ class RangeTest < ActiveSupport::TestCase def test_no_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, 31)..Time.utc(2005, 12, 10, 18, 00) - assert !time_range_1.overlaps?(time_range_2) + assert_not time_range_1.overlaps?(time_range_2) end def test_each_on_time_with_zone twz = ActiveSupport::TimeWithZone.new(nil, ActiveSupport::TimeZone["Eastern Time (US & Canada)"], Time.utc(2006, 11, 28, 10, 30)) assert_raises TypeError do - ((twz - 1.hour)..twz).each {} + ((twz - 1.hour)..twz).each { } end end def test_step_on_time_with_zone twz = ActiveSupport::TimeWithZone.new(nil, ActiveSupport::TimeZone["Eastern Time (US & Canada)"], Time.utc(2006, 11, 28, 10, 30)) assert_raises TypeError do - ((twz - 1.hour)..twz).step(1) {} + ((twz - 1.hour)..twz).step(1) { } end end @@ -131,11 +131,11 @@ class RangeTest < ActiveSupport::TestCase def test_date_time_with_each datetime = DateTime.now - assert(((datetime - 1.hour)..datetime).each {}) + assert(((datetime - 1.hour)..datetime).each { }) end def test_date_time_with_step datetime = DateTime.now - assert(((datetime - 1.hour)..datetime).step(1) {}) + assert(((datetime - 1.hour)..datetime).step(1) { }) end end diff --git a/activesupport/test/core_ext/regexp_ext_test.rb b/activesupport/test/core_ext/regexp_ext_test.rb index 5737bdafda..7d18297b64 100644 --- a/activesupport/test/core_ext/regexp_ext_test.rb +++ b/activesupport/test/core_ext/regexp_ext_test.rb @@ -9,28 +9,4 @@ class RegexpExtAccessTests < ActiveSupport::TestCase assert_equal false, //.multiline? assert_equal false, /(?m:)/.multiline? end - - # Based on https://github.com/ruby/ruby/blob/trunk/test/ruby/test_regexp.rb. - def test_match_p - /back(...)/ =~ "backref" - # must match here, but not in a separate method, e.g., assert_send, - # to check if $~ is affected or not. - assert_equal false, //.match?(nil) - assert_equal true, //.match?("") - assert_equal true, /.../.match?(:abc) - assert_raise(TypeError) { /.../.match?(Object.new) } - assert_equal true, /b/.match?("abc") - assert_equal true, /b/.match?("abc", 1) - assert_equal true, /../.match?("abc", 1) - assert_equal true, /../.match?("abc", -2) - assert_equal false, /../.match?("abc", -4) - assert_equal false, /../.match?("abc", 4) - assert_equal true, /\z/.match?("") - assert_equal true, /\z/.match?("abc") - assert_equal true, /R.../.match?("Ruby") - assert_equal false, /R.../.match?("Ruby", 1) - assert_equal false, /P.../.match?("Ruby") - assert_equal "backref", $& - assert_equal "ref", $1 - end end diff --git a/activesupport/test/core_ext/secure_random_test.rb b/activesupport/test/core_ext/secure_random_test.rb index 7067fb524c..4b73233971 100644 --- a/activesupport/test/core_ext/secure_random_test.rb +++ b/activesupport/test/core_ext/secure_random_test.rb @@ -19,4 +19,24 @@ class SecureRandomTest < ActiveSupport::TestCase assert_not_equal s1, s2 assert_equal 24, s1.length end + + def test_base36 + s1 = SecureRandom.base36 + s2 = SecureRandom.base36 + + assert_not_equal s1, s2 + assert_equal 16, s1.length + assert_match(/^[a-z0-9]+$/, s1) + assert_match(/^[a-z0-9]+$/, s2) + end + + def test_base36_with_length + s1 = SecureRandom.base36(24) + s2 = SecureRandom.base36(24) + + assert_not_equal s1, s2 + assert_equal 24, s1.length + assert_match(/^[a-z0-9]+$/, s1) + assert_match(/^[a-z0-9]+$/, s2) + end end diff --git a/activesupport/test/core_ext/string_ext_test.rb b/activesupport/test/core_ext/string_ext_test.rb index 5c5abe9fd1..2468fe3603 100644 --- a/activesupport/test/core_ext/string_ext_test.rb +++ b/activesupport/test/core_ext/string_ext_test.rb @@ -9,9 +9,9 @@ require "constantize_test_cases" require "active_support/inflector" require "active_support/core_ext/string" require "active_support/time" -require "active_support/core_ext/string/strip" require "active_support/core_ext/string/output_safety" require "active_support/core_ext/string/indent" +require "active_support/core_ext/string/strip" require "time_zone_test_helpers" require "yaml" @@ -24,6 +24,10 @@ class StringInflectionsTest < ActiveSupport::TestCase assert_equal "", "".strip_heredoc end + def test_strip_heredoc_on_a_frozen_string + assert "".strip_heredoc.frozen? + end + def test_strip_heredoc_on_a_string_with_no_lines assert_equal "x", "x".strip_heredoc assert_equal "x", " x".strip_heredoc @@ -233,16 +237,16 @@ class StringInflectionsTest < ActiveSupport::TestCase s = "hello" assert s.starts_with?("h") assert s.starts_with?("hel") - assert !s.starts_with?("el") + assert_not s.starts_with?("el") assert s.ends_with?("o") assert s.ends_with?("lo") - assert !s.ends_with?("el") + assert_not s.ends_with?("el") end 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}.dup + 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} expected = "A string surrounded by various unicode spaces, " \ "with tabs( ), newlines( ), unicode nextlines( ) and many spaces( )." @@ -259,8 +263,8 @@ class StringInflectionsTest < ActiveSupport::TestCase end def test_string_inquiry - assert "production".inquiry.production? - assert !"production".inquiry.development? + assert_predicate "production".inquiry, :production? + assert_not_predicate "production".inquiry, :development? end def test_truncate @@ -281,6 +285,68 @@ class StringInflectionsTest < ActiveSupport::TestCase assert_equal "Hello Big[...]", "Hello Big World!".truncate(15, omission: "[...]", separator: /\s/) end + def test_truncate_bytes + assert_equal "👍👍👍👍", "👍👍👍👍".truncate_bytes(16) + assert_equal "👍👍👍👍", "👍👍👍👍".truncate_bytes(16, omission: nil) + assert_equal "👍👍👍👍", "👍👍👍👍".truncate_bytes(16, omission: " ") + assert_equal "👍👍👍👍", "👍👍👍👍".truncate_bytes(16, omission: "🖖") + + assert_equal "👍👍👍…", "👍👍👍👍".truncate_bytes(15) + assert_equal "👍👍👍", "👍👍👍👍".truncate_bytes(15, omission: nil) + assert_equal "👍👍👍 ", "👍👍👍👍".truncate_bytes(15, omission: " ") + assert_equal "👍👍🖖", "👍👍👍👍".truncate_bytes(15, omission: "🖖") + + assert_equal "…", "👍👍👍👍".truncate_bytes(5) + assert_equal "👍", "👍👍👍👍".truncate_bytes(5, omission: nil) + assert_equal "👍 ", "👍👍👍👍".truncate_bytes(5, omission: " ") + assert_equal "🖖", "👍👍👍👍".truncate_bytes(5, omission: "🖖") + + assert_equal "…", "👍👍👍👍".truncate_bytes(4) + assert_equal "👍", "👍👍👍👍".truncate_bytes(4, omission: nil) + assert_equal " ", "👍👍👍👍".truncate_bytes(4, omission: " ") + assert_equal "🖖", "👍👍👍👍".truncate_bytes(4, omission: "🖖") + + assert_raise ArgumentError do + "👍👍👍👍".truncate_bytes(3, omission: "🖖") + end + end + + def test_truncate_bytes_preserves_codepoints + assert_equal "👍👍👍👍", "👍👍👍👍".truncate_bytes(16) + assert_equal "👍👍👍👍", "👍👍👍👍".truncate_bytes(16, omission: nil) + assert_equal "👍👍👍👍", "👍👍👍👍".truncate_bytes(16, omission: " ") + assert_equal "👍👍👍👍", "👍👍👍👍".truncate_bytes(16, omission: "🖖") + + assert_equal "👍👍👍…", "👍👍👍👍".truncate_bytes(15) + assert_equal "👍👍👍", "👍👍👍👍".truncate_bytes(15, omission: nil) + assert_equal "👍👍👍 ", "👍👍👍👍".truncate_bytes(15, omission: " ") + assert_equal "👍👍🖖", "👍👍👍👍".truncate_bytes(15, omission: "🖖") + + assert_equal "…", "👍👍👍👍".truncate_bytes(5) + assert_equal "👍", "👍👍👍👍".truncate_bytes(5, omission: nil) + assert_equal "👍 ", "👍👍👍👍".truncate_bytes(5, omission: " ") + assert_equal "🖖", "👍👍👍👍".truncate_bytes(5, omission: "🖖") + + assert_equal "…", "👍👍👍👍".truncate_bytes(4) + assert_equal "👍", "👍👍👍👍".truncate_bytes(4, omission: nil) + assert_equal " ", "👍👍👍👍".truncate_bytes(4, omission: " ") + assert_equal "🖖", "👍👍👍👍".truncate_bytes(4, omission: "🖖") + + assert_raise ArgumentError do + "👍👍👍👍".truncate_bytes(3, omission: "🖖") + end + end + + def test_truncates_bytes_preserves_grapheme_clusters + assert_equal "a ", "a ❤️ b".truncate_bytes(2, omission: nil) + assert_equal "a ", "a ❤️ b".truncate_bytes(3, omission: nil) + assert_equal "a ", "a ❤️ b".truncate_bytes(7, omission: nil) + assert_equal "a ❤️", "a ❤️ b".truncate_bytes(8, omission: nil) + + assert_equal "a ", "a 👩❤️👩".truncate_bytes(13, omission: nil) + assert_equal "", "👩❤️👩".truncate_bytes(13, omission: nil) + end + def test_truncate_words assert_equal "Hello Big World!", "Hello Big World!".truncate_words(3) assert_equal "Hello Big...", "Hello Big World!".truncate_words(2) @@ -312,12 +378,12 @@ 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 ...".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) + 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) end def test_truncate_should_not_be_html_safe - assert !"Hello World!".truncate(12).html_safe? + assert_not_predicate "Hello World!".truncate(12), :html_safe? end def test_remove @@ -334,7 +400,7 @@ class StringInflectionsTest < ActiveSupport::TestCase end def test_remove! - original = "This is a very good day to die".dup + original = +"This is a very good day to die" 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/) @@ -403,6 +469,15 @@ class StringAccessTest < ActiveSupport::TestCase assert_not_same different_string, string end + test "#first with negative Integer is deprecated" do + string = "hello" + message = "Calling String#first with a negative integer limit " \ + "will raise an ArgumentError in Rails 6.1." + assert_deprecated(message) do + string.first(-1) + end + end + test "#last returns the last character" do assert_equal "o", "hello".last assert_equal "x", "x".last @@ -421,6 +496,15 @@ class StringAccessTest < ActiveSupport::TestCase assert_not_same different_string, string end + test "#last with negative Integer is deprecated" do + string = "hello" + message = "Calling String#last with a negative integer limit " \ + "will raise an ArgumentError in Rails 6.1." + assert_deprecated(message) do + string.last(-1) + end + end + test "access returns a real string" do hash = {} hash["h"] = true @@ -639,7 +723,7 @@ end class StringBehaviourTest < ActiveSupport::TestCase def test_acts_like_string - assert "Bambi".acts_like_string? + assert_predicate "Bambi", :acts_like_string? end end @@ -654,10 +738,10 @@ class CoreExtStringMultibyteTest < ActiveSupport::TestCase end def test_string_should_recognize_utf8_strings - assert UTF8_STRING.is_utf8? - assert ASCII_STRING.is_utf8? - assert !EUC_JP_STRING.is_utf8? - assert !INVALID_UTF8_STRING.is_utf8? + assert_predicate UTF8_STRING, :is_utf8? + assert_predicate ASCII_STRING, :is_utf8? + assert_not_predicate EUC_JP_STRING, :is_utf8? + assert_not_predicate INVALID_UTF8_STRING, :is_utf8? end def test_mb_chars_returns_instance_of_proxy_class @@ -667,7 +751,7 @@ end class OutputSafetyTest < ActiveSupport::TestCase def setup - @string = "hello".dup + @string = +"hello" @object = Class.new(Object) do def to_s "other" @@ -676,12 +760,12 @@ class OutputSafetyTest < ActiveSupport::TestCase end test "A string is unsafe by default" do - assert !@string.html_safe? + assert_not_predicate @string, :html_safe? end test "A string can be marked safe" do string = @string.html_safe - assert string.html_safe? + assert_predicate string, :html_safe? end test "Marking a string safe returns the string" do @@ -689,15 +773,15 @@ class OutputSafetyTest < ActiveSupport::TestCase end test "An integer is safe by default" do - assert 5.html_safe? + assert_predicate 5, :html_safe? end test "a float is safe by default" do - assert 5.7.html_safe? + assert_predicate 5.7, :html_safe? end test "An object is unsafe by default" do - assert !@object.html_safe? + assert_not_predicate @object, :html_safe? end test "Adding an object to a safe string returns a safe string" do @@ -705,7 +789,7 @@ class OutputSafetyTest < ActiveSupport::TestCase string << @object assert_equal "helloother", string - assert string.html_safe? + assert_predicate string, :html_safe? end test "Adding a safe string to another safe string returns a safe string" do @@ -714,7 +798,7 @@ class OutputSafetyTest < ActiveSupport::TestCase @combination = @other_string + string assert_equal "otherhello", @combination - assert @combination.html_safe? + assert_predicate @combination, :html_safe? end test "Adding an unsafe string to a safe string escapes it and returns a safe string" do @@ -725,36 +809,36 @@ class OutputSafetyTest < ActiveSupport::TestCase assert_equal "other<foo>", @combination assert_equal "hello<foo>", @other_combination - assert @combination.html_safe? - assert !@other_combination.html_safe? + assert_predicate @combination, :html_safe? + assert_not_predicate @other_combination, :html_safe? end test "Prepending safe onto unsafe yields unsafe" do @string.prepend "other".html_safe - assert !@string.html_safe? + assert_not_predicate @string, :html_safe? assert_equal "otherhello", @string end test "Prepending unsafe onto safe yields escaped safe" do other = "other".html_safe other.prepend "<foo>" - assert other.html_safe? + assert_predicate other, :html_safe? assert_equal "<foo>other", other end test "Concatting safe onto unsafe yields unsafe" do - @other_string = "other".dup + @other_string = +"other" string = @string.html_safe @other_string.concat(string) - assert !@other_string.html_safe? + assert_not_predicate @other_string, :html_safe? end test "Concatting unsafe onto safe yields escaped safe" do @other_string = "other".html_safe string = @other_string.concat("<foo>") assert_equal "other<foo>", string - assert string.html_safe? + assert_predicate string, :html_safe? end test "Concatting safe onto safe yields safe" do @@ -762,22 +846,22 @@ class OutputSafetyTest < ActiveSupport::TestCase string = @string.html_safe @other_string.concat(string) - assert @other_string.html_safe? + assert_predicate @other_string, :html_safe? end test "Concatting safe onto unsafe with << yields unsafe" do - @other_string = "other".dup + @other_string = +"other" string = @string.html_safe @other_string << string - assert !@other_string.html_safe? + assert_not_predicate @other_string, :html_safe? end test "Concatting unsafe onto safe with << yields escaped safe" do @other_string = "other".html_safe string = @other_string << "<foo>" assert_equal "other<foo>", string - assert string.html_safe? + assert_predicate string, :html_safe? end test "Concatting safe onto safe with << yields safe" do @@ -785,7 +869,7 @@ class OutputSafetyTest < ActiveSupport::TestCase string = @string.html_safe @other_string << string - assert @other_string.html_safe? + assert_predicate @other_string, :html_safe? end test "Concatting safe onto unsafe with % yields unsafe" do @@ -793,7 +877,7 @@ class OutputSafetyTest < ActiveSupport::TestCase string = @string.html_safe @other_string = @other_string % string - assert !@other_string.html_safe? + assert_not_predicate @other_string, :html_safe? end test "Concatting unsafe onto safe with % yields escaped safe" do @@ -801,7 +885,7 @@ class OutputSafetyTest < ActiveSupport::TestCase string = @other_string % "<foo>" assert_equal "other<foo>", string - assert string.html_safe? + assert_predicate string, :html_safe? end test "Concatting safe onto safe with % yields safe" do @@ -809,7 +893,7 @@ class OutputSafetyTest < ActiveSupport::TestCase string = @string.html_safe @other_string = @other_string % string - assert @other_string.html_safe? + assert_predicate @other_string, :html_safe? end test "Concatting with % doesn't modify a string" do @@ -822,8 +906,56 @@ 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".dup.concat(13), string - assert string.html_safe? + assert_equal (+"hello").concat(13), string + assert_predicate string, :html_safe? + end + + test "Inserting safe into safe yields safe" do + string = "foo".html_safe + string.insert(0, "<b>".html_safe) + + assert_equal "<b>foo", string + assert_predicate string, :html_safe? + end + + test "Inserting unsafe into safe yields escaped safe" do + string = "foo".html_safe + string.insert(0, "<b>") + + assert_equal "<b>foo", string + assert_predicate string, :html_safe? + end + + test "Replacing safe with safe yields safe" do + string = "foo".html_safe + string.replace("<b>".html_safe) + + assert_equal "<b>", string + assert_predicate string, :html_safe? + end + + test "Replacing safe with unsafe yields escaped safe" do + string = "foo".html_safe + string.replace("<b>") + + assert_equal "<b>", string + assert_predicate string, :html_safe? + end + + test "Replacing index of safe with safe yields safe" do + string = "foo".html_safe + string[0] = "<b>".html_safe + + assert_equal "<b>oo", string + assert_predicate string, :html_safe? + end + + test "Replacing index of safe with unsafe yields escaped safe" do + string = "foo".html_safe + string[0] = "<b>" + + assert_equal "<b>oo", string + assert_predicate string, :html_safe? end test "emits normal string yaml" do @@ -832,8 +964,8 @@ class OutputSafetyTest < ActiveSupport::TestCase test "call to_param returns a normal string" do string = @string.html_safe - assert string.html_safe? - assert !string.to_param.html_safe? + assert_predicate string, :html_safe? + assert_not_predicate string.to_param, :html_safe? end test "ERB::Util.html_escape should escape unsafe characters" do diff --git a/activesupport/test/core_ext/time_ext_test.rb b/activesupport/test/core_ext/time_ext_test.rb index 01cf1938be..7078f3506d 100644 --- a/activesupport/test/core_ext/time_ext_test.rb +++ b/activesupport/test/core_ext/time_ext_test.rb @@ -737,7 +737,7 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase end def test_acts_like_time - assert Time.new.acts_like_time? + assert_predicate Time.new, :acts_like_time? end def test_formatted_offset_with_utc @@ -756,26 +756,26 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase end def test_compare_with_time - assert_equal 1, Time.utc(2000) <=> Time.utc(1999, 12, 31, 23, 59, 59, 999) - assert_equal 0, Time.utc(2000) <=> Time.utc(2000, 1, 1, 0, 0, 0) + assert_equal 1, Time.utc(2000) <=> Time.utc(1999, 12, 31, 23, 59, 59, 999) + assert_equal 0, Time.utc(2000) <=> Time.utc(2000, 1, 1, 0, 0, 0) assert_equal(-1, Time.utc(2000) <=> Time.utc(2000, 1, 1, 0, 0, 0, 001)) end def test_compare_with_datetime - assert_equal 1, Time.utc(2000) <=> DateTime.civil(1999, 12, 31, 23, 59, 59) - assert_equal 0, Time.utc(2000) <=> DateTime.civil(2000, 1, 1, 0, 0, 0) + assert_equal 1, Time.utc(2000) <=> DateTime.civil(1999, 12, 31, 23, 59, 59) + assert_equal 0, Time.utc(2000) <=> DateTime.civil(2000, 1, 1, 0, 0, 0) assert_equal(-1, Time.utc(2000) <=> DateTime.civil(2000, 1, 1, 0, 0, 1)) end def test_compare_with_time_with_zone - assert_equal 1, Time.utc(2000) <=> ActiveSupport::TimeWithZone.new(Time.utc(1999, 12, 31, 23, 59, 59), ActiveSupport::TimeZone["UTC"]) - assert_equal 0, Time.utc(2000) <=> ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 1, 0, 0, 0), ActiveSupport::TimeZone["UTC"]) + assert_equal 1, Time.utc(2000) <=> ActiveSupport::TimeWithZone.new(Time.utc(1999, 12, 31, 23, 59, 59), ActiveSupport::TimeZone["UTC"]) + assert_equal 0, Time.utc(2000) <=> ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 1, 0, 0, 0), ActiveSupport::TimeZone["UTC"]) assert_equal(-1, Time.utc(2000) <=> ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 1, 0, 0, 1), ActiveSupport::TimeZone["UTC"])) end def test_compare_with_string - assert_equal 1, Time.utc(2000) <=> Time.utc(1999, 12, 31, 23, 59, 59, 999).to_s - assert_equal 0, Time.utc(2000) <=> Time.utc(2000, 1, 1, 0, 0, 0).to_s + assert_equal 1, Time.utc(2000) <=> Time.utc(1999, 12, 31, 23, 59, 59, 999).to_s + assert_equal 0, Time.utc(2000) <=> Time.utc(2000, 1, 1, 0, 0, 0).to_s assert_equal(-1, Time.utc(2000) <=> Time.utc(2000, 1, 1, 0, 0, 1, 0).to_s) assert_nil Time.utc(2000) <=> "Invalid as Time" end @@ -863,11 +863,11 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase end def test_minus_with_time_with_zone - assert_equal 86_400.0, Time.utc(2000, 1, 2) - ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 1), ActiveSupport::TimeZone["UTC"]) + assert_equal 86_400.0, Time.utc(2000, 1, 2) - ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 1), ActiveSupport::TimeZone["UTC"]) end def test_minus_with_datetime - assert_equal 86_400.0, Time.utc(2000, 1, 2) - DateTime.civil(2000, 1, 1) + assert_equal 86_400.0, Time.utc(2000, 1, 2) - DateTime.civil(2000, 1, 1) end def test_time_created_with_local_constructor_cannot_represent_times_during_hour_skipped_by_dst @@ -875,7 +875,7 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase # On Apr 2 2006 at 2:00AM in US, clocks were moved forward to 3:00AM. # Therefore, 2AM EST doesn't exist for this date; Time.local fails over to 3:00AM EDT assert_equal Time.local(2006, 4, 2, 3), Time.local(2006, 4, 2, 2) - assert Time.local(2006, 4, 2, 2).dst? + assert_predicate Time.local(2006, 4, 2, 2), :dst? end end @@ -950,39 +950,39 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase end class TimeExtMarshalingTest < ActiveSupport::TestCase - def test_marshaling_with_utc_instance + def test_marshalling_with_utc_instance t = Time.utc(2000) - unmarshaled = Marshal.load(Marshal.dump(t)) - assert_equal "UTC", unmarshaled.zone - assert_equal t, unmarshaled + unmarshalled = Marshal.load(Marshal.dump(t)) + assert_equal "UTC", unmarshalled.zone + assert_equal t, unmarshalled end - def test_marshaling_with_local_instance + def test_marshalling_with_local_instance t = Time.local(2000) - unmarshaled = Marshal.load(Marshal.dump(t)) - assert_equal t.zone, unmarshaled.zone - assert_equal t, unmarshaled + unmarshalled = Marshal.load(Marshal.dump(t)) + assert_equal t.zone, unmarshalled.zone + assert_equal t, unmarshalled end - def test_marshaling_with_frozen_utc_instance + def test_marshalling_with_frozen_utc_instance t = Time.utc(2000).freeze - unmarshaled = Marshal.load(Marshal.dump(t)) - assert_equal "UTC", unmarshaled.zone - assert_equal t, unmarshaled + unmarshalled = Marshal.load(Marshal.dump(t)) + assert_equal "UTC", unmarshalled.zone + assert_equal t, unmarshalled end - def test_marshaling_with_frozen_local_instance + def test_marshalling_with_frozen_local_instance t = Time.local(2000).freeze - unmarshaled = Marshal.load(Marshal.dump(t)) - assert_equal t.zone, unmarshaled.zone - assert_equal t, unmarshaled + unmarshalled = Marshal.load(Marshal.dump(t)) + assert_equal t.zone, unmarshalled.zone + assert_equal t, unmarshalled end def test_marshalling_preserves_fractional_seconds t = Time.parse("00:00:00.500") - unmarshaled = Marshal.load(Marshal.dump(t)) - assert_equal t.to_f, unmarshaled.to_f - assert_equal t, unmarshaled + unmarshalled = Marshal.load(Marshal.dump(t)) + assert_equal t.to_f, unmarshalled.to_f + assert_equal t, unmarshalled end def test_last_quarter_on_31st diff --git a/activesupport/test/core_ext/time_with_zone_test.rb b/activesupport/test/core_ext/time_with_zone_test.rb index b25747eadb..f6e836e446 100644 --- a/activesupport/test/core_ext/time_with_zone_test.rb +++ b/activesupport/test/core_ext/time_with_zone_test.rb @@ -3,7 +3,6 @@ require "abstract_unit" require "active_support/time" require "time_zone_test_helpers" -require "active_support/core_ext/string/strip" require "yaml" class TimeWithZoneTest < ActiveSupport::TestCase @@ -163,7 +162,7 @@ class TimeWithZoneTest < ActiveSupport::TestCase end def test_to_yaml - yaml = <<-EOF.strip_heredoc + yaml = <<~EOF --- !ruby/object:ActiveSupport::TimeWithZone utc: 2000-01-01 00:00:00.000000000 Z zone: !ruby/object:ActiveSupport::TimeZone @@ -175,7 +174,7 @@ class TimeWithZoneTest < ActiveSupport::TestCase end def test_ruby_to_yaml - yaml = <<-EOF.strip_heredoc + yaml = <<~EOF --- twz: !ruby/object:ActiveSupport::TimeWithZone utc: 2000-01-01 00:00:00.000000000 Z @@ -188,7 +187,7 @@ class TimeWithZoneTest < ActiveSupport::TestCase end def test_yaml_load - yaml = <<-EOF.strip_heredoc + yaml = <<~EOF --- !ruby/object:ActiveSupport::TimeWithZone utc: 2000-01-01 00:00:00.000000000 Z zone: !ruby/object:ActiveSupport::TimeZone @@ -200,7 +199,7 @@ class TimeWithZoneTest < ActiveSupport::TestCase end def test_ruby_yaml_load - yaml = <<-EOF.strip_heredoc + yaml = <<~EOF --- twz: !ruby/object:ActiveSupport::TimeWithZone utc: 2000-01-01 00:00:00.000000000 Z @@ -221,20 +220,20 @@ class TimeWithZoneTest < ActiveSupport::TestCase end def test_compare_with_time - assert_equal 1, @twz <=> Time.utc(1999, 12, 31, 23, 59, 59) - assert_equal 0, @twz <=> Time.utc(2000, 1, 1, 0, 0, 0) + assert_equal 1, @twz <=> Time.utc(1999, 12, 31, 23, 59, 59) + assert_equal 0, @twz <=> Time.utc(2000, 1, 1, 0, 0, 0) assert_equal(-1, @twz <=> Time.utc(2000, 1, 1, 0, 0, 1)) end def test_compare_with_datetime - assert_equal 1, @twz <=> DateTime.civil(1999, 12, 31, 23, 59, 59) - assert_equal 0, @twz <=> DateTime.civil(2000, 1, 1, 0, 0, 0) + assert_equal 1, @twz <=> DateTime.civil(1999, 12, 31, 23, 59, 59) + assert_equal 0, @twz <=> DateTime.civil(2000, 1, 1, 0, 0, 0) assert_equal(-1, @twz <=> DateTime.civil(2000, 1, 1, 0, 0, 1)) end def test_compare_with_time_with_zone - assert_equal 1, @twz <=> ActiveSupport::TimeWithZone.new(Time.utc(1999, 12, 31, 23, 59, 59), ActiveSupport::TimeZone["UTC"]) - assert_equal 0, @twz <=> ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 1, 0, 0, 0), ActiveSupport::TimeZone["UTC"]) + assert_equal 1, @twz <=> ActiveSupport::TimeWithZone.new(Time.utc(1999, 12, 31, 23, 59, 59), ActiveSupport::TimeZone["UTC"]) + assert_equal 0, @twz <=> ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 1, 0, 0, 0), ActiveSupport::TimeZone["UTC"]) assert_equal(-1, @twz <=> ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 1, 0, 0, 1), ActiveSupport::TimeZone["UTC"])) end @@ -290,6 +289,20 @@ class TimeWithZoneTest < ActiveSupport::TestCase end end + def test_before + twz = ActiveSupport::TimeWithZone.new(Time.utc(2017, 3, 6, 12, 0, 0), @time_zone) + assert_equal false, twz.before?(ActiveSupport::TimeWithZone.new(Time.utc(2017, 3, 6, 11, 59, 59), @time_zone)) + assert_equal false, twz.before?(ActiveSupport::TimeWithZone.new(Time.utc(2017, 3, 6, 12, 0, 0), @time_zone)) + assert_equal true, twz.before?(ActiveSupport::TimeWithZone.new(Time.utc(2017, 3, 6, 12, 00, 1), @time_zone)) + end + + def test_after + twz = ActiveSupport::TimeWithZone.new(Time.utc(2017, 3, 6, 12, 0, 0), @time_zone) + assert_equal true, twz.after?(ActiveSupport::TimeWithZone.new(Time.utc(2017, 3, 6, 11, 59, 59), @time_zone)) + assert_equal false, twz.after?(ActiveSupport::TimeWithZone.new(Time.utc(2017, 3, 6, 12, 0, 0), @time_zone)) + assert_equal false, twz.after?(ActiveSupport::TimeWithZone.new(Time.utc(2017, 3, 6, 12, 00, 1), @time_zone)) + end + def test_eql? assert_equal true, @twz.eql?(@twz.dup) assert_equal true, @twz.eql?(Time.utc(2000)) @@ -340,13 +353,13 @@ class TimeWithZoneTest < ActiveSupport::TestCase end def test_minus_with_time - assert_equal 86_400.0, ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 2), ActiveSupport::TimeZone["UTC"]) - Time.utc(2000, 1, 1) - assert_equal 86_400.0, ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 2), ActiveSupport::TimeZone["Hawaii"]) - Time.utc(2000, 1, 1) + assert_equal 86_400.0, ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 2), ActiveSupport::TimeZone["UTC"]) - Time.utc(2000, 1, 1) + assert_equal 86_400.0, ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 2), ActiveSupport::TimeZone["Hawaii"]) - Time.utc(2000, 1, 1) end def test_minus_with_time_precision - assert_equal 86_399.999999998, ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 2, 23, 59, 59, Rational(999999999, 1000)), ActiveSupport::TimeZone["UTC"]) - Time.utc(2000, 1, 2, 0, 0, 0, Rational(1, 1000)) - assert_equal 86_399.999999998, ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 2, 23, 59, 59, Rational(999999999, 1000)), ActiveSupport::TimeZone["Hawaii"]) - Time.utc(2000, 1, 2, 0, 0, 0, Rational(1, 1000)) + assert_equal 86_399.999999998, ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 2, 23, 59, 59, Rational(999999999, 1000)), ActiveSupport::TimeZone["UTC"]) - Time.utc(2000, 1, 2, 0, 0, 0, Rational(1, 1000)) + assert_equal 86_399.999999998, ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 2, 23, 59, 59, Rational(999999999, 1000)), ActiveSupport::TimeZone["Hawaii"]) - Time.utc(2000, 1, 2, 0, 0, 0, Rational(1, 1000)) end def test_minus_with_time_with_zone @@ -358,20 +371,20 @@ class TimeWithZoneTest < ActiveSupport::TestCase def test_minus_with_time_with_zone_precision twz1 = ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 1, 0, 0, 0, Rational(1, 1000)), ActiveSupport::TimeZone["UTC"]) twz2 = ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 1, 23, 59, 59, Rational(999999999, 1000)), ActiveSupport::TimeZone["UTC"]) - assert_equal 86_399.999999998, twz2 - twz1 + assert_equal 86_399.999999998, twz2 - twz1 end def test_minus_with_datetime - assert_equal 86_400.0, ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 2), ActiveSupport::TimeZone["UTC"]) - DateTime.civil(2000, 1, 1) + assert_equal 86_400.0, ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 2), ActiveSupport::TimeZone["UTC"]) - DateTime.civil(2000, 1, 1) end def test_minus_with_datetime_precision - assert_equal 86_399.999999999, ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 1, 23, 59, 59, Rational(999999999, 1000)), ActiveSupport::TimeZone["UTC"]) - DateTime.civil(2000, 1, 1) + assert_equal 86_399.999999999, ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 1, 23, 59, 59, Rational(999999999, 1000)), ActiveSupport::TimeZone["UTC"]) - DateTime.civil(2000, 1, 1) end def test_minus_with_wrapped_datetime - assert_equal 86_400.0, ActiveSupport::TimeWithZone.new(DateTime.civil(2000, 1, 2), ActiveSupport::TimeZone["UTC"]) - Time.utc(2000, 1, 1) - assert_equal 86_400.0, ActiveSupport::TimeWithZone.new(DateTime.civil(2000, 1, 2), ActiveSupport::TimeZone["UTC"]) - DateTime.civil(2000, 1, 1) + assert_equal 86_400.0, ActiveSupport::TimeWithZone.new(DateTime.civil(2000, 1, 2), ActiveSupport::TimeZone["UTC"]) - Time.utc(2000, 1, 1) + assert_equal 86_400.0, ActiveSupport::TimeWithZone.new(DateTime.civil(2000, 1, 2), ActiveSupport::TimeZone["UTC"]) - DateTime.civil(2000, 1, 1) end def test_plus_and_minus_enforce_spring_dst_rules @@ -481,7 +494,7 @@ class TimeWithZoneTest < ActiveSupport::TestCase end def test_acts_like_time - assert @twz.acts_like_time? + assert_predicate @twz, :acts_like_time? assert @twz.acts_like?(:time) assert ActiveSupport::TimeWithZone.new(DateTime.civil(2000), @time_zone).acts_like?(:time) end @@ -492,7 +505,7 @@ class TimeWithZoneTest < ActiveSupport::TestCase end def test_blank? - assert_not @twz.blank? + assert_not_predicate @twz, :blank? end def test_is_a @@ -514,10 +527,10 @@ class TimeWithZoneTest < ActiveSupport::TestCase marshal_str = Marshal.dump(@twz) mtime = Marshal.load(marshal_str) assert_equal Time.utc(2000, 1, 1, 0), mtime.utc - assert mtime.utc.utc? + assert_predicate mtime.utc, :utc? assert_equal ActiveSupport::TimeZone["Eastern Time (US & Canada)"], mtime.time_zone assert_equal Time.utc(1999, 12, 31, 19), mtime.time - assert mtime.time.utc? + assert_predicate mtime.time, :utc? assert_equal @twz.inspect, mtime.inspect end @@ -526,16 +539,16 @@ class TimeWithZoneTest < ActiveSupport::TestCase marshal_str = Marshal.dump(twz) mtime = Marshal.load(marshal_str) assert_equal Time.utc(2000, 1, 1, 0), mtime.utc - assert mtime.utc.utc? + assert_predicate mtime.utc, :utc? assert_equal "America/New_York", mtime.time_zone.name assert_equal Time.utc(1999, 12, 31, 19), mtime.time - assert mtime.time.utc? + assert_predicate mtime.time, :utc? assert_equal @twz.inspect, mtime.inspect end def test_freeze @twz.freeze - assert @twz.frozen? + assert_predicate @twz, :frozen? end def test_freeze_preloads_instance_variables @@ -1035,8 +1048,8 @@ class TimeWithZoneMethodsForTimeAndDateTimeTest < ActiveSupport::TestCase def test_nil_time_zone Time.use_zone nil do - assert !@t.in_time_zone.respond_to?(:period), "no period method" - assert !@dt.in_time_zone.respond_to?(:period), "no period method" + assert_not_respond_to @t.in_time_zone, :period, "no period method" + assert_not_respond_to @dt.in_time_zone, :period, "no period method" end end @@ -1092,7 +1105,7 @@ class TimeWithZoneMethodsForTimeAndDateTimeTest < ActiveSupport::TestCase def test_use_zone_raises_on_invalid_timezone Time.zone = "Alaska" assert_raise ArgumentError do - Time.use_zone("No such timezone exists") {} + Time.use_zone("No such timezone exists") { } end assert_equal ActiveSupport::TimeZone["Alaska"], Time.zone end @@ -1224,7 +1237,7 @@ class TimeWithZoneMethodsForDate < ActiveSupport::TestCase def test_nil_time_zone with_tz_default nil do - assert !@d.in_time_zone.respond_to?(:period), "no period method" + assert_not_respond_to @d.in_time_zone, :period, "no period method" end end @@ -1273,9 +1286,9 @@ class TimeWithZoneMethodsForString < ActiveSupport::TestCase def test_nil_time_zone with_tz_default nil do - assert !@s.in_time_zone.respond_to?(:period), "no period method" - assert !@u.in_time_zone.respond_to?(:period), "no period method" - assert !@z.in_time_zone.respond_to?(:period), "no period method" + assert_not_respond_to @s.in_time_zone, :period, "no period method" + assert_not_respond_to @u.in_time_zone, :period, "no period method" + assert_not_respond_to @z.in_time_zone, :period, "no period method" end end diff --git a/activesupport/test/core_ext/uri_ext_test.rb b/activesupport/test/core_ext/uri_ext_test.rb index 8816b0d392..c0686bc720 100644 --- a/activesupport/test/core_ext/uri_ext_test.rb +++ b/activesupport/test/core_ext/uri_ext_test.rb @@ -9,6 +9,6 @@ class URIExtTest < ActiveSupport::TestCase str = "\xE6\x97\xA5\xE6\x9C\xAC\xE8\xAA\x9E" # Ni-ho-nn-go in UTF-8, means Japanese. parser = URI.parser - assert_equal str, parser.unescape(parser.escape(str)) + assert_equal str + str, parser.unescape(str + parser.escape(str).encode(Encoding::UTF_8)) end end 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/module_folder/lib_class.rb b/activesupport/test/dependencies/module_folder/lib_class.rb new file mode 100644 index 0000000000..c6b52610c1 --- /dev/null +++ b/activesupport/test/dependencies/module_folder/lib_class.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +ConstFromLib = 1 + +module ModuleFolder + class LibClass + end +end diff --git a/activesupport/test/dependencies_test.rb b/activesupport/test/dependencies_test.rb index d636da46d2..d4e709137e 100644 --- a/activesupport/test/dependencies_test.rb +++ b/activesupport/test/dependencies_test.rb @@ -185,6 +185,61 @@ class DependenciesTest < ActiveSupport::TestCase end end + # Regression see https://github.com/rails/rails/issues/31694 + def test_included_constant_that_changes_to_have_exception_then_back_does_not_loop_forever + # This constant references a nested constant whose namespace will be auto-generated + parent_constant = <<-RUBY + class ConstantReloadError + AnotherConstant::ReloadError + end + RUBY + + # This constant's namespace will be auto-generated, + # also, we'll edit it to contain an error at load-time + child_constant = <<-RUBY + class AnotherConstant::ReloadError + # no_such_method_as_this + end + RUBY + + # Create a version which contains an error during loading + child_constant_with_error = child_constant.sub("# no_such_method_as_this", "no_such_method_as_this") + + fixtures_path = File.join(__dir__, "autoloading_fixtures") + Dir.mktmpdir(nil, fixtures_path) do |tmpdir| + # Set up the file structure where constants will be loaded from + child_constant_path = "#{tmpdir}/another_constant/reload_error.rb" + File.write("#{tmpdir}/constant_reload_error.rb", parent_constant) + Dir.mkdir("#{tmpdir}/another_constant") + File.write(child_constant_path, child_constant_with_error) + + tmpdir_name = tmpdir.split("/").last + with_loading("autoloading_fixtures/#{tmpdir_name}") do + # Load the file, with the error: + assert_raises(NameError) { + ConstantReloadError + } + + Timeout.timeout(0.1) do + # Remove the constant, as if Rails development middleware is reloading changed files: + ActiveSupport::Dependencies.remove_unloadable_constants! + assert_not defined?(AnotherConstant::ReloadError) + end + + # Change the file, so that it is **correct** this time: + File.write(child_constant_path, child_constant) + + # Again: Remove the constant, as if Rails development middleware is reloading changed files: + ActiveSupport::Dependencies.remove_unloadable_constants! + assert_not defined?(AnotherConstant::ReloadError) + + # Now, reload the _fixed_ constant: + assert ConstantReloadError + assert AnotherConstant::ReloadError + end + end + end + def test_module_loading with_autoloading_fixtures do assert_kind_of Module, A @@ -227,6 +282,32 @@ class DependenciesTest < ActiveSupport::TestCase remove_constants(:ModuleFolder) end + def test_module_with_nested_class_requiring_lib_class + with_autoloading_fixtures do + _ = ModuleFolder::NestedWithRequire # assignment to silence parse-time warning "possibly useless use of :: in void context" + + assert defined?(ModuleFolder::LibClass) + assert_not ActiveSupport::Dependencies.autoloaded_constants.include?("ModuleFolder::LibClass") + assert_not ActiveSupport::Dependencies.autoloaded_constants.include?("ConstFromLib") + end + ensure + remove_constants(:ModuleFolder) + remove_constants(:ConstFromLib) + end + + def test_module_with_nested_class_and_parent_requiring_lib_class + with_autoloading_fixtures do + _ = NestedWithRequireParent # assignment to silence parse-time warning "possibly useless use of a constant in void context" + + assert defined?(ModuleFolder::LibClass) + assert_not ActiveSupport::Dependencies.autoloaded_constants.include?("ModuleFolder::LibClass") + assert_not ActiveSupport::Dependencies.autoloaded_constants.include?("ConstFromLib") + end + ensure + remove_constants(:ModuleFolder) + remove_constants(:ConstFromLib) + end + def test_directories_may_manifest_as_nested_classes with_autoloading_fixtures do assert_kind_of Class, ClassFolder @@ -480,9 +561,9 @@ class DependenciesTest < ActiveSupport::TestCase def test_qualified_const_defined_should_not_call_const_missing ModuleWithMissing.missing_count = 0 - assert ! ActiveSupport::Dependencies.qualified_const_defined?("ModuleWithMissing::A") + assert_not ActiveSupport::Dependencies.qualified_const_defined?("ModuleWithMissing::A") assert_equal 0, ModuleWithMissing.missing_count - assert ! ActiveSupport::Dependencies.qualified_const_defined?("ModuleWithMissing::A::B") + assert_not ActiveSupport::Dependencies.qualified_const_defined?("ModuleWithMissing::A::B") assert_equal 0, ModuleWithMissing.missing_count end @@ -492,13 +573,13 @@ class DependenciesTest < ActiveSupport::TestCase def test_autoloaded? with_autoloading_fixtures do - assert ! ActiveSupport::Dependencies.autoloaded?("ModuleFolder") - assert ! ActiveSupport::Dependencies.autoloaded?("ModuleFolder::NestedClass") + assert_not ActiveSupport::Dependencies.autoloaded?("ModuleFolder") + assert_not ActiveSupport::Dependencies.autoloaded?("ModuleFolder::NestedClass") assert ActiveSupport::Dependencies.autoloaded?(ModuleFolder) assert ActiveSupport::Dependencies.autoloaded?("ModuleFolder") - assert ! ActiveSupport::Dependencies.autoloaded?("ModuleFolder::NestedClass") + assert_not ActiveSupport::Dependencies.autoloaded?("ModuleFolder::NestedClass") assert ActiveSupport::Dependencies.autoloaded?(ModuleFolder::NestedClass) @@ -509,11 +590,11 @@ class DependenciesTest < ActiveSupport::TestCase assert ActiveSupport::Dependencies.autoloaded?(:ModuleFolder) # Anonymous modules aren't autoloaded. - assert !ActiveSupport::Dependencies.autoloaded?(Module.new) + assert_not ActiveSupport::Dependencies.autoloaded?(Module.new) nil_name = Module.new def nil_name.name() nil end - assert !ActiveSupport::Dependencies.autoloaded?(nil_name) + assert_not ActiveSupport::Dependencies.autoloaded?(nil_name) end ensure remove_constants(:ModuleFolder) @@ -700,7 +781,7 @@ class DependenciesTest < ActiveSupport::TestCase Object.const_set :EM, Class.new with_autoloading_fixtures do require_dependency "em" - assert ! ActiveSupport::Dependencies.autoloaded?(:EM), "EM shouldn't be marked autoloaded!" + assert_not ActiveSupport::Dependencies.autoloaded?(:EM), "EM shouldn't be marked autoloaded!" ActiveSupport::Dependencies.clear end ensure @@ -723,11 +804,11 @@ class DependenciesTest < ActiveSupport::TestCase M.unloadable ActiveSupport::Dependencies.clear - assert ! defined?(M) + assert_not defined?(M) Object.const_set :M, Module.new ActiveSupport::Dependencies.clear - assert ! defined?(M), "Dependencies should unload unloadable constants each time" + assert_not defined?(M), "Dependencies should unload unloadable constants each time" end end @@ -752,16 +833,16 @@ class DependenciesTest < ActiveSupport::TestCase Object.const_set :C, Class.new { def self.before_remove_const; end } C.unloadable assert_called(C, :before_remove_const, times: 1) do - assert C.respond_to?(:before_remove_const) + assert_respond_to C, :before_remove_const ActiveSupport::Dependencies.clear - assert !defined?(C) + assert_not defined?(C) end ensure remove_constants(:C) end - def test_new_contants_in_without_constants - assert_equal [], (ActiveSupport::Dependencies.new_constants_in(Object) {}) + 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 @@ -837,7 +918,7 @@ class DependenciesTest < ActiveSupport::TestCase def test_new_constants_in_with_illegal_module_name_raises_correct_error assert_raise(NameError) do - ActiveSupport::Dependencies.new_constants_in("Illegal-Name") {} + ActiveSupport::Dependencies.new_constants_in("Illegal-Name") { } end end @@ -925,10 +1006,10 @@ class DependenciesTest < ActiveSupport::TestCase def test_autoload_doesnt_shadow_no_method_error_with_relative_constant with_autoloading_fixtures do - assert !defined?(::RaisesNoMethodError), "::RaisesNoMethodError is defined but it hasn't been referenced yet!" + assert_not defined?(::RaisesNoMethodError), "::RaisesNoMethodError is defined but it hasn't been referenced yet!" 2.times do assert_raise(NoMethodError) { RaisesNoMethodError } - assert !defined?(::RaisesNoMethodError), "::RaisesNoMethodError is defined but it should have failed!" + assert_not defined?(::RaisesNoMethodError), "::RaisesNoMethodError is defined but it should have failed!" end end ensure @@ -937,10 +1018,10 @@ class DependenciesTest < ActiveSupport::TestCase def test_autoload_doesnt_shadow_no_method_error_with_absolute_constant with_autoloading_fixtures do - assert !defined?(::RaisesNoMethodError), "::RaisesNoMethodError is defined but it hasn't been referenced yet!" + assert_not defined?(::RaisesNoMethodError), "::RaisesNoMethodError is defined but it hasn't been referenced yet!" 2.times do assert_raise(NoMethodError) { ::RaisesNoMethodError } - assert !defined?(::RaisesNoMethodError), "::RaisesNoMethodError is defined but it should have failed!" + assert_not defined?(::RaisesNoMethodError), "::RaisesNoMethodError is defined but it should have failed!" end end ensure @@ -965,13 +1046,13 @@ class DependenciesTest < ActiveSupport::TestCase ::RaisesNameError::FooBarBaz.object_id end assert_equal "uninitialized constant RaisesNameError::FooBarBaz", e.message - assert !defined?(::RaisesNameError), "::RaisesNameError is defined but it should have failed!" + assert_not defined?(::RaisesNameError), "::RaisesNameError is defined but it should have failed!" end - assert !defined?(::RaisesNameError) + assert_not defined?(::RaisesNameError) 2.times do assert_raise(NameError) { ::RaisesNameError } - assert !defined?(::RaisesNameError), "::RaisesNameError is defined but it should have failed!" + assert_not defined?(::RaisesNameError), "::RaisesNameError is defined but it should have failed!" end end ensure @@ -1075,3 +1156,52 @@ class DependenciesTest < ActiveSupport::TestCase ActiveSupport::Dependencies.hook! end end + +class DependenciesLogging < ActiveSupport::TestCase + MESSAGE = "message" + + def with_settings(logger, verbose) + original_logger = ActiveSupport::Dependencies.logger + original_verbose = ActiveSupport::Dependencies.verbose + + ActiveSupport::Dependencies.logger = logger + ActiveSupport::Dependencies.verbose = verbose + + yield + ensure + ActiveSupport::Dependencies.logger = original_logger + ActiveSupport::Dependencies.verbose = original_verbose + end + + def fake_logger + Class.new do + def self.debug(message) + message + end + end + end + + test "does not log if the logger is nil and verbose is false" do + with_settings(nil, false) do + assert_nil ActiveSupport::Dependencies.log(MESSAGE) + end + end + + test "does not log if the logger is nil and verbose is true" do + with_settings(nil, true) do + assert_nil ActiveSupport::Dependencies.log(MESSAGE) + end + end + + test "does not log if the logger is set and verbose is false" do + with_settings(fake_logger, false) do + assert_nil ActiveSupport::Dependencies.log(MESSAGE) + end + end + + test "logs if the logger is set and verbose is true" do + with_settings(fake_logger, true) do + assert_equal "autoloading: #{MESSAGE}", ActiveSupport::Dependencies.log(MESSAGE) + end + end +end diff --git a/activesupport/test/deprecation/method_wrappers_test.rb b/activesupport/test/deprecation/method_wrappers_test.rb index 439e117c1d..18729941bc 100644 --- a/activesupport/test/deprecation/method_wrappers_test.rb +++ b/activesupport/test/deprecation/method_wrappers_test.rb @@ -21,6 +21,13 @@ class MethodWrappersTest < ActiveSupport::TestCase end end + def test_deprecate_methods_without_alternate_method + warning = /old_method is deprecated and will be removed from Rails \d.\d./ + ActiveSupport::Deprecation.deprecate_methods(@klass, :old_method) + + assert_deprecated(warning) { assert_equal "abc", @klass.new.old_method } + end + def test_deprecate_methods_warning_default warning = /old_method is deprecated and will be removed from Rails \d.\d \(use new_method instead\)/ ActiveSupport::Deprecation.deprecate_methods(@klass, old_method: :new_method) @@ -55,4 +62,39 @@ class MethodWrappersTest < ActiveSupport::TestCase assert(@klass.private_method_defined?(:old_private_method)) end + + def test_deprecate_class_method + mod = Module.new do + extend self + + def old_method + "abc" + end + end + ActiveSupport::Deprecation.deprecate_methods(mod, old_method: :new_method) + + warning = /old_method is deprecated and will be removed from Rails \d.\d \(use new_method instead\)/ + assert_deprecated(warning) { assert_equal "abc", mod.old_method } + end + + def test_deprecate_method_when_class_extends_module + mod = Module.new do + def old_method + "abc" + end + end + @klass.extend mod + ActiveSupport::Deprecation.deprecate_methods(mod, old_method: :new_method) + + warning = /old_method is deprecated and will be removed from Rails \d.\d \(use new_method instead\)/ + assert_deprecated(warning) { assert_equal "abc", @klass.old_method } + end + + def test_method_with_without_deprecation_is_exposed + ActiveSupport::Deprecation.deprecate_methods(@klass, old_method: :new_method) + + warning = /old_method is deprecated and will be removed from Rails \d.\d \(use new_method instead\)/ + assert_deprecated(warning) { assert_equal "abc", @klass.new.old_method_with_deprecation } + assert_equal "abc", @klass.new.old_method_without_deprecation + end end diff --git a/activesupport/test/deprecation/proxy_wrappers_test.rb b/activesupport/test/deprecation/proxy_wrappers_test.rb index 2f866775f6..9e26052fb4 100644 --- a/activesupport/test/deprecation/proxy_wrappers_test.rb +++ b/activesupport/test/deprecation/proxy_wrappers_test.rb @@ -9,16 +9,16 @@ class ProxyWrappersTest < ActiveSupport::TestCase def test_deprecated_object_proxy_doesnt_wrap_falsy_objects proxy = ActiveSupport::Deprecation::DeprecatedObjectProxy.new(nil, "message") - assert !proxy + assert_not proxy end def test_deprecated_instance_variable_proxy_doesnt_wrap_falsy_objects proxy = ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy.new(nil, :waffles) - assert !proxy + assert_not proxy end def test_deprecated_constant_proxy_doesnt_wrap_falsy_objects proxy = ActiveSupport::Deprecation::DeprecatedConstantProxy.new(Waffles, NewWaffles) - assert !proxy + assert_not proxy end end diff --git a/activesupport/test/deprecation_test.rb b/activesupport/test/deprecation_test.rb index f2267a822f..f25c704586 100644 --- a/activesupport/test/deprecation_test.rb +++ b/activesupport/test/deprecation_test.rb @@ -31,6 +31,9 @@ class Deprecatee def f=(v); end deprecate :f= + deprecate :g + def g; end + module B C = 1 end @@ -158,7 +161,7 @@ class DeprecationTest < ActiveSupport::TestCase stderr_output = capture(:stderr) { assert_nil behavior.call("Some error!", ["call stack!"], "horizon", "gem") } - assert stderr_output.empty? + assert_empty stderr_output end def test_default_notify_behavior @@ -182,6 +185,14 @@ class DeprecationTest < ActiveSupport::TestCase end end + def test_default_invalid_behavior + e = assert_raises(ArgumentError) do + ActiveSupport::Deprecation.behavior = :invalid + end + + assert_equal ":invalid is not a valid deprecation behavior.", e.message + end + def test_deprecated_instance_variable_proxy assert_not_deprecated { @dtc.request.size } @@ -417,6 +428,10 @@ class DeprecationTest < ActiveSupport::TestCase end end + def test_deprecate_work_before_define_method + assert_deprecated { @dtc.g } + end + private def deprecator_with_messages klass = Class.new(ActiveSupport::Deprecation) diff --git a/activesupport/test/descendants_tracker_test_cases.rb b/activesupport/test/descendants_tracker_test_cases.rb index 1f8b4a8605..2c94c3c56c 100644 --- a/activesupport/test/descendants_tracker_test_cases.rb +++ b/activesupport/test/descendants_tracker_test_cases.rb @@ -37,7 +37,7 @@ module DescendantsTrackerTestCases mark_as_autoloaded(*ALL) do ActiveSupport::DescendantsTracker.clear ALL.each do |k| - assert ActiveSupport::DescendantsTracker.descendants(k).empty? + assert_empty ActiveSupport::DescendantsTracker.descendants(k) end end end diff --git a/activesupport/test/descendants_tracker_with_autoloading_test.rb b/activesupport/test/descendants_tracker_with_autoloading_test.rb index 7c396b7c8e..d4fedb5a67 100644 --- a/activesupport/test/descendants_tracker_with_autoloading_test.rb +++ b/activesupport/test/descendants_tracker_with_autoloading_test.rb @@ -12,7 +12,7 @@ class DescendantsTrackerWithAutoloadingTest < ActiveSupport::TestCase mark_as_autoloaded(*ALL) do ActiveSupport::DescendantsTracker.clear ALL.each do |k| - assert ActiveSupport::DescendantsTracker.descendants(k).empty? + assert_empty ActiveSupport::DescendantsTracker.descendants(k) end end end diff --git a/activesupport/test/descendants_tracker_without_autoloading_test.rb b/activesupport/test/descendants_tracker_without_autoloading_test.rb index f5c6a3045d..c65f69cba3 100644 --- a/activesupport/test/descendants_tracker_without_autoloading_test.rb +++ b/activesupport/test/descendants_tracker_without_autoloading_test.rb @@ -13,7 +13,7 @@ class DescendantsTrackerWithoutAutoloadingTest < ActiveSupport::TestCase parent_instance = Parent.new parent_instance.singleton_class.descendants ActiveSupport::DescendantsTracker.clear - assert !ActiveSupport::DescendantsTracker.class_variable_get(:@@direct_descendants).key?(parent_instance.singleton_class) + assert_not ActiveSupport::DescendantsTracker.class_variable_get(:@@direct_descendants).key?(parent_instance.singleton_class) end end end diff --git a/activesupport/test/digest_test.rb b/activesupport/test/digest_test.rb index 5dec75b9fe..83ff2a8d83 100644 --- a/activesupport/test/digest_test.rb +++ b/activesupport/test/digest_test.rb @@ -16,7 +16,7 @@ class DigestTest < ActiveSupport::TestCase digest = ActiveSupport::Digest.hexdigest("hello friend") assert_equal 32, digest.length - assert_equal ::Digest::SHA1.hexdigest("hello friend").truncate(32), digest + assert_equal ::Digest::SHA1.hexdigest("hello friend")[0...32], digest ensure ActiveSupport::Digest.hash_digest_class = original_hash_digest_class end diff --git a/activesupport/test/encrypted_configuration_test.rb b/activesupport/test/encrypted_configuration_test.rb index 0bc915be82..387d6e1c1f 100644 --- a/activesupport/test/encrypted_configuration_test.rb +++ b/activesupport/test/encrypted_configuration_test.rb @@ -10,8 +10,10 @@ class EncryptedConfigurationTest < ActiveSupport::TestCase @credentials_key_path = File.join(Dir.tmpdir, "master.key") File.write(@credentials_key_path, ActiveSupport::EncryptedConfiguration.generate_key) - @credentials = ActiveSupport::EncryptedConfiguration.new \ - config_path: @credentials_config_path, key_path: @credentials_key_path, env_key: "RAILS_MASTER_KEY" + @credentials = ActiveSupport::EncryptedConfiguration.new( + config_path: @credentials_config_path, key_path: @credentials_key_path, + env_key: "RAILS_MASTER_KEY", raise_if_missing_key: true + ) end teardown do @@ -40,6 +42,12 @@ class EncryptedConfigurationTest < ActiveSupport::TestCase assert @credentials.something[:good] end + test "reading comment-only configuration" do + @credentials.write("# comment") + + assert_equal @credentials.config, {} + end + test "change configuration by key file" do @credentials.write({ something: { good: true } }.to_yaml) @credentials.change do |config_file| diff --git a/activesupport/test/encrypted_file_test.rb b/activesupport/test/encrypted_file_test.rb index 7259726d08..ba3bbef903 100644 --- a/activesupport/test/encrypted_file_test.rb +++ b/activesupport/test/encrypted_file_test.rb @@ -12,8 +12,9 @@ class EncryptedFileTest < ActiveSupport::TestCase @key_path = File.join(Dir.tmpdir, "content.txt.key") File.write(@key_path, ActiveSupport::EncryptedFile.generate_key) - @encrypted_file = ActiveSupport::EncryptedFile.new \ - content_path: @content_path, key_path: @key_path, env_key: "CONTENT_KEY" + @encrypted_file = ActiveSupport::EncryptedFile.new( + content_path: @content_path, key_path: @key_path, env_key: "CONTENT_KEY", raise_if_missing_key: true + ) end teardown do @@ -47,4 +48,12 @@ class EncryptedFileTest < ActiveSupport::TestCase assert_equal "#{@content} and went by the lake", @encrypted_file.read end + + test "raise MissingKeyError when key is missing" do + assert_raise(ActiveSupport::EncryptedFile::MissingKeyError) do + ActiveSupport::EncryptedFile.new( + content_path: @content_path, key_path: "", env_key: "", raise_if_missing_key: true + ).read + end + end end diff --git a/activesupport/test/evented_file_update_checker_test.rb b/activesupport/test/evented_file_update_checker_test.rb index 9b560f7f42..b2d5eb94c2 100644 --- a/activesupport/test/evented_file_update_checker_test.rb +++ b/activesupport/test/evented_file_update_checker_test.rb @@ -38,19 +38,19 @@ class EventedFileUpdateCheckerTest < ActiveSupport::TestCase FileUtils.touch(tmpfiles) - checker = new_checker(tmpfiles) {} - assert !checker.updated? + checker = new_checker(tmpfiles) { } + assert_not_predicate checker, :updated? # Pipes used for flow control across fork. boot_reader, boot_writer = IO.pipe touch_reader, touch_writer = IO.pipe pid = fork do - assert checker.updated? + assert_predicate checker, :updated? # Clear previous check value. checker.execute - assert !checker.updated? + assert_not_predicate checker, :updated? # Fork is booted, ready for file to be touched # notify parent process. @@ -60,7 +60,7 @@ class EventedFileUpdateCheckerTest < ActiveSupport::TestCase # has been touched. IO.select([touch_reader]) - assert checker.updated? + assert_predicate checker, :updated? end assert pid @@ -72,10 +72,38 @@ class EventedFileUpdateCheckerTest < ActiveSupport::TestCase # Notify fork that files have been touched. touch_writer.write("touched") - assert checker.updated? + assert_predicate checker, :updated? Process.wait(pid) end + + test "updated should become true when nonexistent directory is added later" do + Dir.mktmpdir do |dir| + watched_dir = File.join(dir, "app") + unwatched_dir = File.join(dir, "node_modules") + not_exist_watched_dir = File.join(dir, "test") + + Dir.mkdir(watched_dir) + Dir.mkdir(unwatched_dir) + + checker = new_checker([], watched_dir => ".rb", not_exist_watched_dir => ".rb") { } + + FileUtils.touch(File.join(watched_dir, "a.rb")) + wait + assert_predicate checker, :updated? + assert checker.execute_if_updated + + Dir.mkdir(not_exist_watched_dir) + wait + assert_predicate checker, :updated? + assert checker.execute_if_updated + + FileUtils.touch(File.join(unwatched_dir, "a.rb")) + wait + assert_not_predicate checker, :updated? + assert_not checker.execute_if_updated + end + end end class EventedFileUpdateCheckerPathHelperTest < ActiveSupport::TestCase diff --git a/activesupport/test/executor_test.rb b/activesupport/test/executor_test.rb index af441064dd..3026f002c3 100644 --- a/activesupport/test/executor_test.rb +++ b/activesupport/test/executor_test.rb @@ -23,7 +23,7 @@ class ExecutorTest < ActiveSupport::TestCase executor.to_run { @foo = true } executor.to_complete { result = @foo } - executor.wrap {} + executor.wrap { } assert result end @@ -85,7 +85,7 @@ class ExecutorTest < ActiveSupport::TestCase executor.register_hook(hook) - executor.wrap {} + executor.wrap { } assert_equal :some_state, supplied_state end @@ -105,7 +105,7 @@ class ExecutorTest < ActiveSupport::TestCase executor.register_hook(hook) - executor.wrap {} + executor.wrap { } assert_nil supplied_state end @@ -129,7 +129,7 @@ class ExecutorTest < ActiveSupport::TestCase executor.register_hook(hook) assert_raises(DummyError) do - executor.wrap {} + executor.wrap { } end assert_equal :none, supplied_state @@ -154,7 +154,7 @@ class ExecutorTest < ActiveSupport::TestCase end assert_raises(DummyError) do - executor.wrap {} + executor.wrap { } end assert_equal :some_state, supplied_state @@ -187,7 +187,7 @@ class ExecutorTest < ActiveSupport::TestCase executor.register_hook(hook_class.new(:c), outer: true) executor.register_hook(hook_class.new(:d)) - executor.wrap {} + executor.wrap { } assert_equal [:run_c, :run_a, :run_b, :run_d, :complete_a, :complete_b, :complete_d, :complete_c], invoked assert_equal [:state_a, :state_b, :state_d, :state_c], supplied_state @@ -209,9 +209,9 @@ class ExecutorTest < ActiveSupport::TestCase executor.register_hook(hook) before = RubyVM.stat(:class_serial) - executor.wrap {} - executor.wrap {} - executor.wrap {} + executor.wrap { } + executor.wrap { } + executor.wrap { } after = RubyVM.stat(:class_serial) assert_equal before, after diff --git a/activesupport/test/file_update_checker_shared_tests.rb b/activesupport/test/file_update_checker_shared_tests.rb index f8266dac06..72683816b3 100644 --- a/activesupport/test/file_update_checker_shared_tests.rb +++ b/activesupport/test/file_update_checker_shared_tests.rb @@ -30,7 +30,7 @@ module FileUpdateCheckerSharedTests checker = new_checker { i += 1 } - assert !checker.execute_if_updated + assert_not checker.execute_if_updated assert_equal 0, i end @@ -41,7 +41,7 @@ module FileUpdateCheckerSharedTests checker = new_checker(tmpfiles) { i += 1 } - assert !checker.execute_if_updated + assert_not checker.execute_if_updated assert_equal 0, i end @@ -89,12 +89,12 @@ module FileUpdateCheckerSharedTests i = 0 checker = new_checker(tmpfiles) { i += 1 } - assert !checker.updated? + assert_not_predicate checker, :updated? touch(tmpfiles) wait - assert checker.updated? + assert_predicate checker, :updated? end test "updated should become true when watched files are modified" do @@ -103,12 +103,12 @@ module FileUpdateCheckerSharedTests FileUtils.touch(tmpfiles) checker = new_checker(tmpfiles) { i += 1 } - assert !checker.updated? + assert_not_predicate checker, :updated? touch(tmpfiles) wait - assert checker.updated? + assert_predicate checker, :updated? end test "updated should become true when watched files are deleted" do @@ -117,12 +117,12 @@ module FileUpdateCheckerSharedTests FileUtils.touch(tmpfiles) checker = new_checker(tmpfiles) { i += 1 } - assert !checker.updated? + assert_not_predicate checker, :updated? rm_f(tmpfiles) wait - assert checker.updated? + assert_predicate checker, :updated? end test "should be robust to handle files with wrong modified time" do @@ -164,14 +164,14 @@ module FileUpdateCheckerSharedTests i = 0 checker = new_checker(tmpfiles) { i += 1 } - assert !checker.updated? + assert_not_predicate checker, :updated? touch(tmpfiles) wait - assert checker.updated? + assert_predicate checker, :updated? checker.execute - assert !checker.updated? + assert_not_predicate checker, :updated? end test "should execute the block if files change in a watched directory one extension" do @@ -212,7 +212,7 @@ module FileUpdateCheckerSharedTests touch(tmpfile("foo.rb")) wait - assert !checker.execute_if_updated + assert_not checker.execute_if_updated assert_equal 0, i end @@ -238,7 +238,7 @@ module FileUpdateCheckerSharedTests mkdir(subdir) wait - assert !checker.execute_if_updated + assert_not checker.execute_if_updated assert_equal 0, i touch(File.join(subdir, "nested.rb")) @@ -259,7 +259,7 @@ module FileUpdateCheckerSharedTests touch(tmpfile("new.txt")) wait - assert !checker.execute_if_updated + assert_not checker.execute_if_updated assert_equal 0, i # subdir does not look for Ruby files, but its parent tmpdir does. diff --git a/activesupport/test/fixtures/concern/some_concern.rb b/activesupport/test/fixtures/concern/some_concern.rb new file mode 100644 index 0000000000..87f660a81e --- /dev/null +++ b/activesupport/test/fixtures/concern/some_concern.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +require "active_support/concern" + +module SomeConcern + extend ActiveSupport::Concern + + included do + # shouldn't raise when module is loaded more than once + end +end diff --git a/activesupport/test/gzip_test.rb b/activesupport/test/gzip_test.rb index 05ce12fe86..3d790f69c4 100644 --- a/activesupport/test/gzip_test.rb +++ b/activesupport/test/gzip_test.rb @@ -18,7 +18,7 @@ class GzipTest < ActiveSupport::TestCase compressed = ActiveSupport::Gzip.compress("") assert_equal Encoding.find("binary"), compressed.encoding - assert !compressed.blank?, "a compressed blank string should not be blank" + assert_not compressed.blank?, "a compressed blank string should not be blank" end def test_compress_should_return_gzipped_string_by_compression_level diff --git a/activesupport/test/hash_with_indifferent_access_test.rb b/activesupport/test/hash_with_indifferent_access_test.rb index 41d653fa59..1d6a07ec5f 100644 --- a/activesupport/test/hash_with_indifferent_access_test.rb +++ b/activesupport/test/hash_with_indifferent_access_test.rb @@ -57,6 +57,13 @@ class HashWithIndifferentAccessTest < ActiveSupport::TestCase assert_equal @symbols, @mixed.with_indifferent_access.symbolize_keys end + def test_to_options_for_hash_with_indifferent_access + assert_instance_of Hash, @symbols.with_indifferent_access.to_options + assert_equal @symbols, @symbols.with_indifferent_access.to_options + assert_equal @symbols, @strings.with_indifferent_access.to_options + assert_equal @symbols, @mixed.with_indifferent_access.to_options + end + def test_deep_symbolize_keys_for_hash_with_indifferent_access assert_instance_of Hash, @nested_symbols.with_indifferent_access.deep_symbolize_keys assert_equal @nested_symbols, @nested_symbols.with_indifferent_access.deep_symbolize_keys @@ -169,8 +176,6 @@ class HashWithIndifferentAccessTest < ActiveSupport::TestCase 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") @@ -282,7 +287,7 @@ class HashWithIndifferentAccessTest < ActiveSupport::TestCase replaced = hash.replace(b: 12) assert hash.key?("b") - assert !hash.key?(:a) + assert_not hash.key?(:a) assert_equal 12, hash[:b] assert_same hash, replaced end @@ -294,7 +299,7 @@ class HashWithIndifferentAccessTest < ActiveSupport::TestCase replaced = hash.replace(HashByConversion.new(b: 12)) assert hash.key?("b") - assert !hash.key?(:a) + assert_not hash.key?(:a) assert_equal 12, hash[:b] assert_same hash, replaced end @@ -404,6 +409,12 @@ class HashWithIndifferentAccessTest < ActiveSupport::TestCase assert_equal({ "aa" => 1, "bb" => 2 }, hash) assert_instance_of ActiveSupport::HashWithIndifferentAccess, hash + + hash = ActiveSupport::HashWithIndifferentAccess.new(@strings).transform_keys { |k| k.to_sym } + + assert_equal(1, hash[:a]) + assert_equal(1, hash["a"]) + assert_instance_of ActiveSupport::HashWithIndifferentAccess, hash end def test_indifferent_transform_keys_bang @@ -412,6 +423,13 @@ class HashWithIndifferentAccessTest < ActiveSupport::TestCase assert_equal({ "aa" => 1, "bb" => 2 }, indifferent_strings) assert_instance_of ActiveSupport::HashWithIndifferentAccess, indifferent_strings + + indifferent_strings = ActiveSupport::HashWithIndifferentAccess.new(@strings) + indifferent_strings.transform_keys! { |k| k.to_sym } + + assert_equal(1, indifferent_strings[:a]) + assert_equal(1, indifferent_strings["a"]) + assert_instance_of ActiveSupport::HashWithIndifferentAccess, indifferent_strings end def test_indifferent_transform_values @@ -429,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) @@ -460,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) @@ -562,7 +588,6 @@ class HashWithIndifferentAccessTest < ActiveSupport::TestCase end def test_nested_dig_indifferent_access - skip if RUBY_VERSION < "2.3.0" data = { "this" => { "views" => 1234 } }.with_indifferent_access assert_equal 1234, data.dig(:this, :views) end @@ -596,7 +621,7 @@ class HashWithIndifferentAccessTest < ActiveSupport::TestCase def test_assorted_keys_not_stringified original = { Object.new => 2, 1 => 2, [] => true } indiff = original.with_indifferent_access - assert(!indiff.keys.any? { |k| k.kind_of? String }, "A key was converted to a string!") + assert_not(indiff.keys.any? { |k| k.kind_of? String }, "A key was converted to a string!") end def test_deep_merge_on_indifferent_access @@ -662,6 +687,17 @@ class HashWithIndifferentAccessTest < ActiveSupport::TestCase assert_equal "bender", slice["login"] end + def test_indifferent_without + original = { a: "x", b: "y", c: 10 }.with_indifferent_access + expected = { c: 10 }.with_indifferent_access + + [["a", "b"], [:a, :b]].each do |keys| + # Should return a new hash without the given keys. + assert_equal expected, original.without(*keys), keys.inspect + assert_not_equal expected, original + end + end + def test_indifferent_extract original = { :a => 1, "b" => 2, :c => 3, "d" => 4 }.with_indifferent_access expected = { a: 1, b: 2 }.with_indifferent_access diff --git a/activesupport/test/inflector_test.rb b/activesupport/test/inflector_test.rb index 0e3e576a70..c3e1faff5d 100644 --- a/activesupport/test/inflector_test.rb +++ b/activesupport/test/inflector_test.rb @@ -224,12 +224,6 @@ class InflectorTest < ActiveSupport::TestCase assert_equal("json_html_api", ActiveSupport::Inflector.underscore("JSONHTMLAPI")) end - def test_acronym_regexp_is_deprecated - assert_deprecated do - ActiveSupport::Inflector.inflections.acronym_regex - end - end - def test_underscore CamelToUnderscore.each do |camel, underscore| assert_equal(underscore, ActiveSupport::Inflector.underscore(camel)) @@ -460,12 +454,12 @@ class InflectorTest < ActiveSupport::TestCase 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? + assert_empty ActiveSupport::Inflector.inflections(:es).plurals + assert_empty ActiveSupport::Inflector.inflections(:es).singulars + assert_empty ActiveSupport::Inflector.inflections(:es).uncountables + assert_not_empty ActiveSupport::Inflector.inflections.plurals + assert_not_empty ActiveSupport::Inflector.inflections.singulars + assert_not_empty ActiveSupport::Inflector.inflections.uncountables end def test_clear_all @@ -478,10 +472,10 @@ class InflectorTest < ActiveSupport::TestCase inflect.clear :all - assert inflect.plurals.empty? - assert inflect.singulars.empty? - assert inflect.uncountables.empty? - assert inflect.humans.empty? + assert_empty inflect.plurals + assert_empty inflect.singulars + assert_empty inflect.uncountables + assert_empty inflect.humans end end @@ -495,10 +489,10 @@ class InflectorTest < ActiveSupport::TestCase inflect.clear - assert inflect.plurals.empty? - assert inflect.singulars.empty? - assert inflect.uncountables.empty? - assert inflect.humans.empty? + assert_empty inflect.plurals + assert_empty inflect.singulars + assert_empty inflect.uncountables + assert_empty inflect.humans end end diff --git a/activesupport/test/json/encoding_test.rb b/activesupport/test/json/encoding_test.rb index 340a2abf75..7589ffd0ea 100644 --- a/activesupport/test/json/encoding_test.rb +++ b/activesupport/test/json/encoding_test.rb @@ -3,7 +3,6 @@ require "securerandom" require "abstract_unit" require "active_support/core_ext/string/inflections" -require "active_support/core_ext/regexp" require "active_support/json" require "active_support/time" require "time_zone_test_helpers" @@ -22,20 +21,18 @@ class TestJSONEncoding < ActiveSupport::TestCase JSONTest::EncodingTestCases.constants.each do |class_tests| define_method("test_#{class_tests[0..-6].underscore}") do - begin - prev = ActiveSupport.use_standard_json_time_format - - standard_class_tests = /Standard/.match?(class_tests) - - ActiveSupport.escape_html_entities_in_json = !standard_class_tests - ActiveSupport.use_standard_json_time_format = standard_class_tests - JSONTest::EncodingTestCases.const_get(class_tests).each do |pair| - assert_equal pair.last, sorted_json(ActiveSupport::JSON.encode(pair.first)) - end - ensure - ActiveSupport.escape_html_entities_in_json = false - ActiveSupport.use_standard_json_time_format = prev + prev = ActiveSupport.use_standard_json_time_format + + standard_class_tests = /Standard/.match?(class_tests) + + ActiveSupport.escape_html_entities_in_json = !standard_class_tests + ActiveSupport.use_standard_json_time_format = standard_class_tests + JSONTest::EncodingTestCases.const_get(class_tests).each do |pair| + assert_equal pair.last, sorted_json(ActiveSupport::JSON.encode(pair.first)) end + ensure + ActiveSupport.escape_html_entities_in_json = false + ActiveSupport.use_standard_json_time_format = prev end end @@ -158,6 +155,16 @@ class TestJSONEncoding < ActiveSupport::TestCase assert_equal({ "foo" => "hello" }, JSON.parse(json)) end + def test_struct_to_json_with_options_nested + klass = Struct.new(:foo, :bar) + struct = klass.new "hello", "world" + parent_struct = klass.new struct, "world" + json = parent_struct.to_json only: [:foo] + + assert_equal({ "foo" => { "foo" => "hello" } }, JSON.parse(json)) + end + + def test_hash_should_pass_encoding_options_to_children_in_as_json person = { name: "John", diff --git a/activesupport/test/key_generator_test.rb b/activesupport/test/key_generator_test.rb index a948cfbd8e..9dfc0b2154 100644 --- a/activesupport/test/key_generator_test.rb +++ b/activesupport/test/key_generator_test.rb @@ -9,9 +9,6 @@ rescue LoadError, NameError $stderr.puts "Skipping KeyGenerator test: broken OpenSSL install" else - require "active_support/time" - require "active_support/json" - class KeyGeneratorTest < ActiveSupport::TestCase def setup @secret = SecureRandom.hex(64) @@ -36,13 +33,13 @@ else # key would break. expected = "b129376f68f1ecae788d7433310249d65ceec090ecacd4c872a3a9e9ec78e055739be5cc6956345d5ae38e7e1daa66f1de587dc8da2bf9e8b965af4b3918a122" - assert_equal expected, ActiveSupport::KeyGenerator.new("0" * 64).generate_key("some_salt").unpack("H*").first + assert_equal expected, ActiveSupport::KeyGenerator.new("0" * 64).generate_key("some_salt").unpack1("H*") expected = "b129376f68f1ecae788d7433310249d65ceec090ecacd4c872a3a9e9ec78e055" - assert_equal expected, ActiveSupport::KeyGenerator.new("0" * 64).generate_key("some_salt", 32).unpack("H*").first + assert_equal expected, ActiveSupport::KeyGenerator.new("0" * 64).generate_key("some_salt", 32).unpack1("H*") expected = "cbea7f7f47df705967dc508f4e446fd99e7797b1d70011c6899cd39bbe62907b8508337d678505a7dc8184e037f1003ba3d19fc5d829454668e91d2518692eae" - assert_equal expected, ActiveSupport::KeyGenerator.new("0" * 64, iterations: 2).generate_key("some_salt").unpack("H*").first + assert_equal expected, ActiveSupport::KeyGenerator.new("0" * 64, iterations: 2).generate_key("some_salt").unpack1("H*") end end diff --git a/activesupport/test/lazy_load_hooks_test.rb b/activesupport/test/lazy_load_hooks_test.rb index 721d44d0c1..50a703e49f 100644 --- a/activesupport/test/lazy_load_hooks_test.rb +++ b/activesupport/test/lazy_load_hooks_test.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require "abstract_unit" +require "active_support/core_ext/module/remove_method" class LazyLoadHooksTest < ActiveSupport::TestCase def test_basic_hook @@ -125,6 +126,54 @@ class LazyLoadHooksTest < ActiveSupport::TestCase assert_equal 7, i end + def test_hook_uses_class_eval_when_base_is_a_class + ActiveSupport.on_load(:uses_class_eval) do + def first_wrestler + "John Cena" + end + end + + ActiveSupport.run_load_hooks(:uses_class_eval, FakeContext) + assert_equal "John Cena", FakeContext.new(0).first_wrestler + ensure + FakeContext.remove_possible_method(:first_wrestler) + end + + def test_hook_uses_class_eval_when_base_is_a_module + mod = Module.new + ActiveSupport.on_load(:uses_class_eval2) do + def last_wrestler + "Dwayne Johnson" + end + end + ActiveSupport.run_load_hooks(:uses_class_eval2, mod) + + klass = Class.new do + include mod + end + + assert_equal "Dwayne Johnson", klass.new.last_wrestler + end + + def test_hook_uses_instance_eval_when_base_is_an_instance + ActiveSupport.on_load(:uses_instance_eval) do + def second_wrestler + "Hulk Hogan" + end + end + + context = FakeContext.new(1) + ActiveSupport.run_load_hooks(:uses_instance_eval, context) + + assert_raises NoMethodError do + FakeContext.new(2).second_wrestler + end + assert_raises NoMethodError do + FakeContext.second_wrestler + end + assert_equal "Hulk Hogan", context.second_wrestler + end + private def incr_amt diff --git a/activesupport/test/log_subscriber_test.rb b/activesupport/test/log_subscriber_test.rb index 2af9b1de30..7f05459493 100644 --- a/activesupport/test/log_subscriber_test.rb +++ b/activesupport/test/log_subscriber_test.rb @@ -75,6 +75,22 @@ class SyncLogSubscriberTest < ActiveSupport::TestCase assert_kind_of ActiveSupport::Notifications::Event, @log_subscriber.event end + def test_event_attributes + ActiveSupport::LogSubscriber.attach_to :my_log_subscriber, @log_subscriber + instrument "some_event.my_log_subscriber" + wait + event = @log_subscriber.event + if defined?(JRUBY_VERSION) + assert_equal 0, event.cpu_time + assert_equal 0, event.allocations + else + assert_operator event.cpu_time, :>, 0 + assert_operator event.allocations, :>, 0 + end + assert_operator event.duration, :>, 0 + assert_operator event.idle_time, :>, 0 + end + def test_does_not_send_the_event_if_it_doesnt_match_the_class ActiveSupport::LogSubscriber.attach_to :my_log_subscriber, @log_subscriber instrument "unknown_event.my_log_subscriber" diff --git a/activesupport/test/logger_test.rb b/activesupport/test/logger_test.rb index 5efbd10a7d..160e1156b6 100644 --- a/activesupport/test/logger_test.rb +++ b/activesupport/test/logger_test.rb @@ -5,6 +5,7 @@ require "multibyte_test_helpers" require "stringio" require "fileutils" require "tempfile" +require "tmpdir" require "concurrent/atomics" class LoggerTest < ActiveSupport::TestCase @@ -39,7 +40,7 @@ class LoggerTest < ActiveSupport::TestCase logger = Logger.new f logger.level = Logger::DEBUG - str = "\x80".dup + str = +"\x80" str.force_encoding("ASCII-8BIT") logger.add Logger::DEBUG, str @@ -57,7 +58,7 @@ class LoggerTest < ActiveSupport::TestCase logger = Logger.new f logger.level = Logger::DEBUG - str = "\x80".dup + str = +"\x80" str.force_encoding("ASCII-8BIT") logger.add Logger::DEBUG, str diff --git a/activesupport/test/message_verifier_test.rb b/activesupport/test/message_verifier_test.rb index 05d5c1cbc3..0fa53695e0 100644 --- a/activesupport/test/message_verifier_test.rb +++ b/activesupport/test/message_verifier_test.rb @@ -25,12 +25,12 @@ class MessageVerifierTest < ActiveSupport::TestCase def test_valid_message data, hash = @verifier.generate(@data).split("--") - assert !@verifier.valid_message?(nil) - assert !@verifier.valid_message?("") - assert !@verifier.valid_message?("\xff") # invalid encoding - assert !@verifier.valid_message?("#{data.reverse}--#{hash}") - assert !@verifier.valid_message?("#{data}--#{hash.reverse}") - assert !@verifier.valid_message?("purejunk") + assert_not @verifier.valid_message?(nil) + assert_not @verifier.valid_message?("") + assert_not @verifier.valid_message?("\xff") # invalid encoding + assert_not @verifier.valid_message?("#{data.reverse}--#{hash}") + assert_not @verifier.valid_message?("#{data}--#{hash.reverse}") + assert_not @verifier.valid_message?("purejunk") end def test_simple_round_tripping @@ -40,7 +40,7 @@ class MessageVerifierTest < ActiveSupport::TestCase end def test_verified_returns_false_on_invalid_message - assert !@verifier.verified("purejunk") + assert_not @verifier.verified("purejunk") end def test_verify_exception_on_invalid_message diff --git a/activesupport/test/metadata/shared_metadata_tests.rb b/activesupport/test/metadata/shared_metadata_tests.rb index 08bb0c648e..cf571223e5 100644 --- a/activesupport/test/metadata/shared_metadata_tests.rb +++ b/activesupport/test/metadata/shared_metadata_tests.rb @@ -1,11 +1,6 @@ # frozen_string_literal: true module SharedMessageMetadataTests - def teardown - travel_back - super - end - def null_serializing? false end diff --git a/activesupport/test/multibyte_chars_test.rb b/activesupport/test/multibyte_chars_test.rb index f51fbe2671..5f4e3f3fd3 100644 --- a/activesupport/test/multibyte_chars_test.rb +++ b/activesupport/test/multibyte_chars_test.rb @@ -53,7 +53,7 @@ class MultibyteCharsTest < ActiveSupport::TestCase end def test_forwarded_method_with_non_string_result_should_be_returned_verbatim - str = "".dup + str = +"" 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 } @@ -61,26 +61,32 @@ class MultibyteCharsTest < ActiveSupport::TestCase end def test_should_concatenate - mb_a = "a".dup.mb_chars - mb_b = "b".dup.mb_chars + mb_a = (+"a").mb_chars + mb_b = (+"b").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".dup << mb_b + assert_equal "ab", (+"a") << mb_b assert_equal "abb", mb_a << mb_b end def test_consumes_utf8_strings - assert @proxy_class.consumes?(UNICODE_STRING) - assert @proxy_class.consumes?(ASCII_STRING) - assert !@proxy_class.consumes?(BYTE_STRING) + ActiveSupport::Deprecation.silence do + assert @proxy_class.consumes?(UNICODE_STRING) + assert @proxy_class.consumes?(ASCII_STRING) + assert_not @proxy_class.consumes?(BYTE_STRING) + end + end + + def test_consumes_is_deprecated + assert_deprecated { @proxy_class.consumes?(UNICODE_STRING) } end 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".dup.mb_chars << "b").class + assert_equal ActiveSupport::Multibyte.proxy_class, ((+"a").mb_chars << "b").class end def test_ascii_strings_are_treated_at_utf8_strings @@ -90,8 +96,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".dup.mb_chars << "b").kind_of?(@proxy_class)) - assert(("a".dup.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)) end def test_should_return_string_as_json @@ -135,7 +141,7 @@ class MultibyteCharsUTF8BehaviourTest < ActiveSupport::TestCase end def test_tidy_bytes_bang_should_change_wrapped_string - original = " Un bUen café \x92".dup + original = +" Un bUen café \x92" proxy = chars(original.dup) proxy.tidy_bytes! assert_not_equal original, proxy.to_s @@ -148,11 +154,11 @@ class MultibyteCharsUTF8BehaviourTest < ActiveSupport::TestCase def test_identity assert_equal @chars, @chars assert @chars.eql?(@chars) - assert !@chars.eql?(UNICODE_STRING) + assert_not @chars.eql?(UNICODE_STRING) end def test_string_methods_are_chainable - assert chars("".dup).insert(0, "").kind_of?(ActiveSupport::Multibyte.proxy_class) + assert chars(+"").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) @@ -165,7 +171,9 @@ class MultibyteCharsUTF8BehaviourTest < ActiveSupport::TestCase assert chars("").upcase.kind_of?(ActiveSupport::Multibyte.proxy_class) assert chars("").downcase.kind_of?(ActiveSupport::Multibyte.proxy_class) assert chars("").capitalize.kind_of?(ActiveSupport::Multibyte.proxy_class) - assert chars("").normalize.kind_of?(ActiveSupport::Multibyte.proxy_class) + ActiveSupport::Deprecation.silence do + assert chars("").normalize.kind_of?(ActiveSupport::Multibyte.proxy_class) + end assert chars("").decompose.kind_of?(ActiveSupport::Multibyte.proxy_class) assert chars("").compose.kind_of?(ActiveSupport::Multibyte.proxy_class) assert chars("").tidy_bytes.kind_of?(ActiveSupport::Multibyte.proxy_class) @@ -197,7 +205,7 @@ class MultibyteCharsUTF8BehaviourTest < ActiveSupport::TestCase end def test_should_use_character_offsets_for_insert_offsets - assert_equal "", "".dup.mb_chars.insert(0, "") + assert_equal "", (+"").mb_chars.insert(0, "") assert_equal "こわにちわ", @chars.insert(1, "わ") assert_equal "こわわわにちわ", @chars.insert(2, "わわ") assert_equal "わこわわわにちわ", @chars.insert(0, "わ") @@ -383,10 +391,12 @@ class MultibyteCharsUTF8BehaviourTest < ActiveSupport::TestCase def test_reverse_should_work_with_normalized_strings str = "bös" reversed_str = "söb" - assert_equal chars(reversed_str).normalize(:kc), chars(str).normalize(:kc).reverse - assert_equal chars(reversed_str).normalize(:c), chars(str).normalize(:c).reverse - assert_equal chars(reversed_str).normalize(:d), chars(str).normalize(:d).reverse - assert_equal chars(reversed_str).normalize(:kd), chars(str).normalize(:kd).reverse + ActiveSupport::Deprecation.silence do + assert_equal chars(reversed_str).normalize(:kc), chars(str).normalize(:kc).reverse + assert_equal chars(reversed_str).normalize(:c), chars(str).normalize(:c).reverse + assert_equal chars(reversed_str).normalize(:d), chars(str).normalize(:d).reverse + assert_equal chars(reversed_str).normalize(:kd), chars(str).normalize(:kd).reverse + end assert_equal chars(reversed_str).decompose, chars(str).decompose.reverse assert_equal chars(reversed_str).compose, chars(str).compose.reverse end @@ -420,13 +430,13 @@ class MultibyteCharsUTF8BehaviourTest < ActiveSupport::TestCase end def test_slice_bang_removes_the_slice_from_the_receiver - chars = "úüù".dup.mb_chars + chars = (+"úüù").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 = "úüù".dup + string = +"úüù" chars = string.mb_chars assert_nil chars.slice!(4, 5) assert_equal "úüù", chars @@ -469,15 +479,15 @@ class MultibyteCharsUTF8BehaviourTest < ActiveSupport::TestCase end def test_respond_to_knows_which_methods_the_proxy_responds_to - assert "".mb_chars.respond_to?(:slice) # Defined on Chars - assert "".mb_chars.respond_to?(:capitalize!) # Defined on Chars - assert "".mb_chars.respond_to?(:gsub) # Defined on String - assert !"".mb_chars.respond_to?(:undefined_method) # Not defined + assert_respond_to "".mb_chars, :slice # Defined on Chars + assert_respond_to "".mb_chars, :capitalize! # Defined on Chars + assert_respond_to "".mb_chars, :gsub # Defined on String + assert_not_respond_to "".mb_chars, :undefined_method # Not defined end def test_method_works_for_proxyed_methods assert_equal "ll", "hello".mb_chars.method(:slice).call(2..3) # Defined on Chars - chars = "hello".mb_chars + chars = +"hello".mb_chars assert_equal "Hello", chars.method(:capitalize!).call # Defined on Chars assert_equal "Hello", chars assert_equal "jello", "hello".mb_chars.method(:gsub).call(/h/, "j") # Defined on String @@ -485,7 +495,7 @@ class MultibyteCharsUTF8BehaviourTest < ActiveSupport::TestCase end def test_acts_like_string - assert "Bambi".mb_chars.acts_like_string? + assert_predicate "Bambi".mb_chars, :acts_like_string? end end @@ -568,7 +578,9 @@ class MultibyteCharsExtrasTest < ActiveSupport::TestCase def test_composition_exclusion_is_set_up_properly # Normalization of DEVANAGARI LETTER QA breaks when composition exclusion isn't used correctly qa = [0x915, 0x93c].pack("U*") - assert_equal qa, chars(qa).normalize(:c) + ActiveSupport::Deprecation.silence do + assert_equal qa, chars(qa).normalize(:c) + end end # Test for the Public Review Issue #29, bad explanation of composition might lead to a @@ -578,17 +590,21 @@ class MultibyteCharsExtrasTest < ActiveSupport::TestCase [0x0B47, 0x0300, 0x0B3E], [0x1100, 0x0300, 0x1161] ].map { |c| c.pack("U*") }.each do |c| - assert_equal_codepoints c, chars(c).normalize(:c) + ActiveSupport::Deprecation.silence do + assert_equal_codepoints c, chars(c).normalize(:c) + end end end def test_normalization_shouldnt_strip_null_bytes null_byte_str = "Test\0test" - assert_equal null_byte_str, chars(null_byte_str).normalize(:kc) - assert_equal null_byte_str, chars(null_byte_str).normalize(:c) - assert_equal null_byte_str, chars(null_byte_str).normalize(:d) - assert_equal null_byte_str, chars(null_byte_str).normalize(:kd) + ActiveSupport::Deprecation.silence do + assert_equal null_byte_str, chars(null_byte_str).normalize(:kc) + assert_equal null_byte_str, chars(null_byte_str).normalize(:c) + assert_equal null_byte_str, chars(null_byte_str).normalize(:d) + assert_equal null_byte_str, chars(null_byte_str).normalize(:kd) + end assert_equal null_byte_str, chars(null_byte_str).decompose assert_equal null_byte_str, chars(null_byte_str).compose end @@ -601,11 +617,13 @@ class MultibyteCharsExtrasTest < ActiveSupport::TestCase 323 # COMBINING DOT BELOW ].pack("U*") - assert_equal_codepoints "", chars("").normalize - assert_equal_codepoints [44, 105, 106, 328, 323].pack("U*"), chars(comp_str).normalize(:kc).to_s - assert_equal_codepoints [44, 307, 328, 323].pack("U*"), chars(comp_str).normalize(:c).to_s - assert_equal_codepoints [44, 307, 110, 780, 78, 769].pack("U*"), chars(comp_str).normalize(:d).to_s - assert_equal_codepoints [44, 105, 106, 110, 780, 78, 769].pack("U*"), chars(comp_str).normalize(:kd).to_s + ActiveSupport::Deprecation.silence do + assert_equal_codepoints "", chars("").normalize + assert_equal_codepoints [44, 105, 106, 328, 323].pack("U*"), chars(comp_str).normalize(:kc).to_s + assert_equal_codepoints [44, 307, 328, 323].pack("U*"), chars(comp_str).normalize(:c).to_s + assert_equal_codepoints [44, 307, 110, 780, 78, 769].pack("U*"), chars(comp_str).normalize(:d).to_s + assert_equal_codepoints [44, 105, 106, 110, 780, 78, 769].pack("U*"), chars(comp_str).normalize(:kd).to_s + end end def test_should_compute_grapheme_length @@ -719,6 +737,51 @@ class MultibyteCharsExtrasTest < ActiveSupport::TestCase assert_equal BYTE_STRING.dup.mb_chars.class, ActiveSupport::Multibyte::Chars end + def test_unicode_normalize_deprecation + # String#unicode_normalize default form is `:nfc`, and + # different than Multibyte::Unicode default, `:nkfc`. + # Deprecation should suggest the right form if no params + # are given and default is used. + assert_deprecated(/unicode_normalize\(:nfkc\)/) do + ActiveSupport::Multibyte::Unicode.normalize("") + end + + assert_deprecated(/unicode_normalize\(:nfd\)/) do + ActiveSupport::Multibyte::Unicode.normalize("", :d) + end + end + + def test_chars_normalize_deprecation + # String#unicode_normalize default form is `:nfc`, and + # different than Multibyte::Unicode default, `:nkfc`. + # Deprecation should suggest the right form if no params + # are given and default is used. + assert_deprecated(/unicode_normalize\(:nfkc\)/) do + "".mb_chars.normalize + end + + assert_deprecated(/unicode_normalize\(:nfc\)/) { "".mb_chars.normalize(:c) } + assert_deprecated(/unicode_normalize\(:nfd\)/) { "".mb_chars.normalize(:d) } + assert_deprecated(/unicode_normalize\(:nfkc\)/) { "".mb_chars.normalize(:kc) } + assert_deprecated(/unicode_normalize\(:nfkd\)/) { "".mb_chars.normalize(:kd) } + end + + def test_unicode_deprecations + assert_deprecated { ActiveSupport::Multibyte::Unicode.downcase("") } + assert_deprecated { ActiveSupport::Multibyte::Unicode.upcase("") } + assert_deprecated { ActiveSupport::Multibyte::Unicode.swapcase("") } + end + + def test_normalize_non_unicode_string + # Fullwidth Latin Capital Letter A in Windows 31J + str = "\u{ff21}".encode(Encoding::Windows_31J) + assert_raise Encoding::CompatibilityError do + ActiveSupport::Deprecation.silence do + ActiveSupport::Multibyte::Unicode.normalize(str) + end + end + end + private def string_from_classes(classes) @@ -732,21 +795,3 @@ class MultibyteCharsExtrasTest < ActiveSupport::TestCase end.pack("U*") end end - -class MultibyteInternalsTest < ActiveSupport::TestCase - include MultibyteTestHelpers - - test "Chars translates a character offset to a byte offset" do - example = chars("Puisque c'était son erreur, il m'a aidé") - [ - [0, 0], - [3, 3], - [12, 11], - [14, 13], - [41, 39] - ].each do |byte_offset, character_offset| - assert_equal character_offset, example.send(:translate_offset, byte_offset), - "Expected byte offset #{byte_offset} to translate to #{character_offset}" - end - end -end diff --git a/activesupport/test/multibyte_conformance_test.rb b/activesupport/test/multibyte_conformance_test.rb index 748e8d16e1..b19689723f 100644 --- a/activesupport/test/multibyte_conformance_test.rb +++ b/activesupport/test/multibyte_conformance_test.rb @@ -3,15 +3,10 @@ require "abstract_unit" require "multibyte_test_helpers" -require "fileutils" -require "open-uri" -require "tmpdir" - class MultibyteConformanceTest < ActiveSupport::TestCase include MultibyteTestHelpers UNIDATA_FILE = "/NormalizationTest.txt" - FileUtils.mkdir_p(CACHE_DIR) RUN_P = begin Downloader.download(UNIDATA_URL + UNIDATA_FILE, CACHE_DIR + UNIDATA_FILE) rescue @@ -23,64 +18,72 @@ class MultibyteConformanceTest < ActiveSupport::TestCase end def test_normalizations_C - each_line_of_norm_tests do |*cols| - col1, col2, col3, col4, col5, comment = *cols + ActiveSupport::Deprecation.silence do + each_line_of_norm_tests do |*cols| + col1, col2, col3, col4, col5, comment = *cols - # CONFORMANCE: - # 1. The following invariants must be true for all conformant implementations - # - # NFC - # c2 == NFC(c1) == NFC(c2) == NFC(c3) - assert_equal_codepoints col2, @proxy.new(col1).normalize(:c), "Form C - Col 2 has to be NFC(1) - #{comment}" - assert_equal_codepoints col2, @proxy.new(col2).normalize(:c), "Form C - Col 2 has to be NFC(2) - #{comment}" - assert_equal_codepoints col2, @proxy.new(col3).normalize(:c), "Form C - Col 2 has to be NFC(3) - #{comment}" - # - # c4 == NFC(c4) == NFC(c5) - assert_equal_codepoints col4, @proxy.new(col4).normalize(:c), "Form C - Col 4 has to be C(4) - #{comment}" - assert_equal_codepoints col4, @proxy.new(col5).normalize(:c), "Form C - Col 4 has to be C(5) - #{comment}" + # CONFORMANCE: + # 1. The following invariants must be true for all conformant implementations + # + # NFC + # c2 == NFC(c1) == NFC(c2) == NFC(c3) + assert_equal_codepoints col2, @proxy.new(col1).normalize(:c), "Form C - Col 2 has to be NFC(1) - #{comment}" + assert_equal_codepoints col2, @proxy.new(col2).normalize(:c), "Form C - Col 2 has to be NFC(2) - #{comment}" + assert_equal_codepoints col2, @proxy.new(col3).normalize(:c), "Form C - Col 2 has to be NFC(3) - #{comment}" + # + # c4 == NFC(c4) == NFC(c5) + assert_equal_codepoints col4, @proxy.new(col4).normalize(:c), "Form C - Col 4 has to be C(4) - #{comment}" + assert_equal_codepoints col4, @proxy.new(col5).normalize(:c), "Form C - Col 4 has to be C(5) - #{comment}" + end end end def test_normalizations_D - each_line_of_norm_tests do |*cols| - col1, col2, col3, col4, col5, comment = *cols - # - # NFD - # c3 == NFD(c1) == NFD(c2) == NFD(c3) - assert_equal_codepoints col3, @proxy.new(col1).normalize(:d), "Form D - Col 3 has to be NFD(1) - #{comment}" - assert_equal_codepoints col3, @proxy.new(col2).normalize(:d), "Form D - Col 3 has to be NFD(2) - #{comment}" - assert_equal_codepoints col3, @proxy.new(col3).normalize(:d), "Form D - Col 3 has to be NFD(3) - #{comment}" - # c5 == NFD(c4) == NFD(c5) - assert_equal_codepoints col5, @proxy.new(col4).normalize(:d), "Form D - Col 5 has to be NFD(4) - #{comment}" - assert_equal_codepoints col5, @proxy.new(col5).normalize(:d), "Form D - Col 5 has to be NFD(5) - #{comment}" + ActiveSupport::Deprecation.silence do + each_line_of_norm_tests do |*cols| + col1, col2, col3, col4, col5, comment = *cols + # + # NFD + # c3 == NFD(c1) == NFD(c2) == NFD(c3) + assert_equal_codepoints col3, @proxy.new(col1).normalize(:d), "Form D - Col 3 has to be NFD(1) - #{comment}" + assert_equal_codepoints col3, @proxy.new(col2).normalize(:d), "Form D - Col 3 has to be NFD(2) - #{comment}" + assert_equal_codepoints col3, @proxy.new(col3).normalize(:d), "Form D - Col 3 has to be NFD(3) - #{comment}" + # c5 == NFD(c4) == NFD(c5) + assert_equal_codepoints col5, @proxy.new(col4).normalize(:d), "Form D - Col 5 has to be NFD(4) - #{comment}" + assert_equal_codepoints col5, @proxy.new(col5).normalize(:d), "Form D - Col 5 has to be NFD(5) - #{comment}" + end end end def test_normalizations_KC - each_line_of_norm_tests do | *cols | - col1, col2, col3, col4, col5, comment = *cols - # - # NFKC - # c4 == NFKC(c1) == NFKC(c2) == NFKC(c3) == NFKC(c4) == NFKC(c5) - assert_equal_codepoints col4, @proxy.new(col1).normalize(:kc), "Form D - Col 4 has to be NFKC(1) - #{comment}" - assert_equal_codepoints col4, @proxy.new(col2).normalize(:kc), "Form D - Col 4 has to be NFKC(2) - #{comment}" - assert_equal_codepoints col4, @proxy.new(col3).normalize(:kc), "Form D - Col 4 has to be NFKC(3) - #{comment}" - assert_equal_codepoints col4, @proxy.new(col4).normalize(:kc), "Form D - Col 4 has to be NFKC(4) - #{comment}" - assert_equal_codepoints col4, @proxy.new(col5).normalize(:kc), "Form D - Col 4 has to be NFKC(5) - #{comment}" + ActiveSupport::Deprecation.silence do + each_line_of_norm_tests do | *cols | + col1, col2, col3, col4, col5, comment = *cols + # + # NFKC + # c4 == NFKC(c1) == NFKC(c2) == NFKC(c3) == NFKC(c4) == NFKC(c5) + assert_equal_codepoints col4, @proxy.new(col1).normalize(:kc), "Form D - Col 4 has to be NFKC(1) - #{comment}" + assert_equal_codepoints col4, @proxy.new(col2).normalize(:kc), "Form D - Col 4 has to be NFKC(2) - #{comment}" + assert_equal_codepoints col4, @proxy.new(col3).normalize(:kc), "Form D - Col 4 has to be NFKC(3) - #{comment}" + assert_equal_codepoints col4, @proxy.new(col4).normalize(:kc), "Form D - Col 4 has to be NFKC(4) - #{comment}" + assert_equal_codepoints col4, @proxy.new(col5).normalize(:kc), "Form D - Col 4 has to be NFKC(5) - #{comment}" + end end end def test_normalizations_KD - each_line_of_norm_tests do | *cols | - col1, col2, col3, col4, col5, comment = *cols - # - # NFKD - # c5 == NFKD(c1) == NFKD(c2) == NFKD(c3) == NFKD(c4) == NFKD(c5) - assert_equal_codepoints col5, @proxy.new(col1).normalize(:kd), "Form KD - Col 5 has to be NFKD(1) - #{comment}" - assert_equal_codepoints col5, @proxy.new(col2).normalize(:kd), "Form KD - Col 5 has to be NFKD(2) - #{comment}" - assert_equal_codepoints col5, @proxy.new(col3).normalize(:kd), "Form KD - Col 5 has to be NFKD(3) - #{comment}" - assert_equal_codepoints col5, @proxy.new(col4).normalize(:kd), "Form KD - Col 5 has to be NFKD(4) - #{comment}" - assert_equal_codepoints col5, @proxy.new(col5).normalize(:kd), "Form KD - Col 5 has to be NFKD(5) - #{comment}" + ActiveSupport::Deprecation.silence do + each_line_of_norm_tests do | *cols | + col1, col2, col3, col4, col5, comment = *cols + # + # NFKD + # c5 == NFKD(c1) == NFKD(c2) == NFKD(c3) == NFKD(c4) == NFKD(c5) + assert_equal_codepoints col5, @proxy.new(col1).normalize(:kd), "Form KD - Col 5 has to be NFKD(1) - #{comment}" + assert_equal_codepoints col5, @proxy.new(col2).normalize(:kd), "Form KD - Col 5 has to be NFKD(2) - #{comment}" + assert_equal_codepoints col5, @proxy.new(col3).normalize(:kd), "Form KD - Col 5 has to be NFKD(3) - #{comment}" + assert_equal_codepoints col5, @proxy.new(col4).normalize(:kd), "Form KD - Col 5 has to be NFKD(4) - #{comment}" + assert_equal_codepoints col5, @proxy.new(col5).normalize(:kd), "Form KD - Col 5 has to be NFKD(5) - #{comment}" + end end end diff --git a/activesupport/test/multibyte_grapheme_break_conformance_test.rb b/activesupport/test/multibyte_grapheme_break_conformance_test.rb index fac74cd80f..97963279af 100644 --- a/activesupport/test/multibyte_grapheme_break_conformance_test.rb +++ b/activesupport/test/multibyte_grapheme_break_conformance_test.rb @@ -3,10 +3,6 @@ require "abstract_unit" require "multibyte_test_helpers" -require "fileutils" -require "open-uri" -require "tmpdir" - class MultibyteGraphemeBreakConformanceTest < ActiveSupport::TestCase include MultibyteTestHelpers @@ -21,10 +17,12 @@ class MultibyteGraphemeBreakConformanceTest < ActiveSupport::TestCase end def test_breaks - each_line_of_break_tests do |*cols| - *clusters, comment = *cols - packed = ActiveSupport::Multibyte::Unicode.pack_graphemes(clusters) - assert_equal clusters, ActiveSupport::Multibyte::Unicode.unpack_graphemes(packed), comment + ActiveSupport::Deprecation.silence do + each_line_of_break_tests do |*cols| + *clusters, comment = *cols + packed = ActiveSupport::Multibyte::Unicode.pack_graphemes(clusters) + assert_equal clusters, ActiveSupport::Multibyte::Unicode.unpack_graphemes(packed), comment + end end end diff --git a/activesupport/test/multibyte_normalization_conformance_test.rb b/activesupport/test/multibyte_normalization_conformance_test.rb index 1173a94e81..82edf69294 100644 --- a/activesupport/test/multibyte_normalization_conformance_test.rb +++ b/activesupport/test/multibyte_normalization_conformance_test.rb @@ -3,10 +3,6 @@ require "abstract_unit" require "multibyte_test_helpers" -require "fileutils" -require "open-uri" -require "tmpdir" - class MultibyteNormalizationConformanceTest < ActiveSupport::TestCase include MultibyteTestHelpers @@ -22,64 +18,72 @@ class MultibyteNormalizationConformanceTest < ActiveSupport::TestCase end def test_normalizations_C - each_line_of_norm_tests do |*cols| - col1, col2, col3, col4, col5, comment = *cols + ActiveSupport::Deprecation.silence do + each_line_of_norm_tests do |*cols| + col1, col2, col3, col4, col5, comment = *cols - # CONFORMANCE: - # 1. The following invariants must be true for all conformant implementations - # - # NFC - # c2 == NFC(c1) == NFC(c2) == NFC(c3) - assert_equal_codepoints col2, @proxy.new(col1).normalize(:c), "Form C - Col 2 has to be NFC(1) - #{comment}" - assert_equal_codepoints col2, @proxy.new(col2).normalize(:c), "Form C - Col 2 has to be NFC(2) - #{comment}" - assert_equal_codepoints col2, @proxy.new(col3).normalize(:c), "Form C - Col 2 has to be NFC(3) - #{comment}" - # - # c4 == NFC(c4) == NFC(c5) - assert_equal_codepoints col4, @proxy.new(col4).normalize(:c), "Form C - Col 4 has to be C(4) - #{comment}" - assert_equal_codepoints col4, @proxy.new(col5).normalize(:c), "Form C - Col 4 has to be C(5) - #{comment}" + # CONFORMANCE: + # 1. The following invariants must be true for all conformant implementations + # + # NFC + # c2 == NFC(c1) == NFC(c2) == NFC(c3) + assert_equal_codepoints col2, @proxy.new(col1).normalize(:c), "Form C - Col 2 has to be NFC(1) - #{comment}" + assert_equal_codepoints col2, @proxy.new(col2).normalize(:c), "Form C - Col 2 has to be NFC(2) - #{comment}" + assert_equal_codepoints col2, @proxy.new(col3).normalize(:c), "Form C - Col 2 has to be NFC(3) - #{comment}" + # + # c4 == NFC(c4) == NFC(c5) + assert_equal_codepoints col4, @proxy.new(col4).normalize(:c), "Form C - Col 4 has to be C(4) - #{comment}" + assert_equal_codepoints col4, @proxy.new(col5).normalize(:c), "Form C - Col 4 has to be C(5) - #{comment}" + end end end def test_normalizations_D - each_line_of_norm_tests do |*cols| - col1, col2, col3, col4, col5, comment = *cols - # - # NFD - # c3 == NFD(c1) == NFD(c2) == NFD(c3) - assert_equal_codepoints col3, @proxy.new(col1).normalize(:d), "Form D - Col 3 has to be NFD(1) - #{comment}" - assert_equal_codepoints col3, @proxy.new(col2).normalize(:d), "Form D - Col 3 has to be NFD(2) - #{comment}" - assert_equal_codepoints col3, @proxy.new(col3).normalize(:d), "Form D - Col 3 has to be NFD(3) - #{comment}" - # c5 == NFD(c4) == NFD(c5) - assert_equal_codepoints col5, @proxy.new(col4).normalize(:d), "Form D - Col 5 has to be NFD(4) - #{comment}" - assert_equal_codepoints col5, @proxy.new(col5).normalize(:d), "Form D - Col 5 has to be NFD(5) - #{comment}" + ActiveSupport::Deprecation.silence do + each_line_of_norm_tests do |*cols| + col1, col2, col3, col4, col5, comment = *cols + # + # NFD + # c3 == NFD(c1) == NFD(c2) == NFD(c3) + assert_equal_codepoints col3, @proxy.new(col1).normalize(:d), "Form D - Col 3 has to be NFD(1) - #{comment}" + assert_equal_codepoints col3, @proxy.new(col2).normalize(:d), "Form D - Col 3 has to be NFD(2) - #{comment}" + assert_equal_codepoints col3, @proxy.new(col3).normalize(:d), "Form D - Col 3 has to be NFD(3) - #{comment}" + # c5 == NFD(c4) == NFD(c5) + assert_equal_codepoints col5, @proxy.new(col4).normalize(:d), "Form D - Col 5 has to be NFD(4) - #{comment}" + assert_equal_codepoints col5, @proxy.new(col5).normalize(:d), "Form D - Col 5 has to be NFD(5) - #{comment}" + end end end def test_normalizations_KC - each_line_of_norm_tests do | *cols | - col1, col2, col3, col4, col5, comment = *cols - # - # NFKC - # c4 == NFKC(c1) == NFKC(c2) == NFKC(c3) == NFKC(c4) == NFKC(c5) - assert_equal_codepoints col4, @proxy.new(col1).normalize(:kc), "Form D - Col 4 has to be NFKC(1) - #{comment}" - assert_equal_codepoints col4, @proxy.new(col2).normalize(:kc), "Form D - Col 4 has to be NFKC(2) - #{comment}" - assert_equal_codepoints col4, @proxy.new(col3).normalize(:kc), "Form D - Col 4 has to be NFKC(3) - #{comment}" - assert_equal_codepoints col4, @proxy.new(col4).normalize(:kc), "Form D - Col 4 has to be NFKC(4) - #{comment}" - assert_equal_codepoints col4, @proxy.new(col5).normalize(:kc), "Form D - Col 4 has to be NFKC(5) - #{comment}" + ActiveSupport::Deprecation.silence do + each_line_of_norm_tests do | *cols | + col1, col2, col3, col4, col5, comment = *cols + # + # NFKC + # c4 == NFKC(c1) == NFKC(c2) == NFKC(c3) == NFKC(c4) == NFKC(c5) + assert_equal_codepoints col4, @proxy.new(col1).normalize(:kc), "Form D - Col 4 has to be NFKC(1) - #{comment}" + assert_equal_codepoints col4, @proxy.new(col2).normalize(:kc), "Form D - Col 4 has to be NFKC(2) - #{comment}" + assert_equal_codepoints col4, @proxy.new(col3).normalize(:kc), "Form D - Col 4 has to be NFKC(3) - #{comment}" + assert_equal_codepoints col4, @proxy.new(col4).normalize(:kc), "Form D - Col 4 has to be NFKC(4) - #{comment}" + assert_equal_codepoints col4, @proxy.new(col5).normalize(:kc), "Form D - Col 4 has to be NFKC(5) - #{comment}" + end end end def test_normalizations_KD - each_line_of_norm_tests do | *cols | - col1, col2, col3, col4, col5, comment = *cols - # - # NFKD - # c5 == NFKD(c1) == NFKD(c2) == NFKD(c3) == NFKD(c4) == NFKD(c5) - assert_equal_codepoints col5, @proxy.new(col1).normalize(:kd), "Form KD - Col 5 has to be NFKD(1) - #{comment}" - assert_equal_codepoints col5, @proxy.new(col2).normalize(:kd), "Form KD - Col 5 has to be NFKD(2) - #{comment}" - assert_equal_codepoints col5, @proxy.new(col3).normalize(:kd), "Form KD - Col 5 has to be NFKD(3) - #{comment}" - assert_equal_codepoints col5, @proxy.new(col4).normalize(:kd), "Form KD - Col 5 has to be NFKD(4) - #{comment}" - assert_equal_codepoints col5, @proxy.new(col5).normalize(:kd), "Form KD - Col 5 has to be NFKD(5) - #{comment}" + ActiveSupport::Deprecation.silence do + each_line_of_norm_tests do | *cols | + col1, col2, col3, col4, col5, comment = *cols + # + # NFKD + # c5 == NFKD(c1) == NFKD(c2) == NFKD(c3) == NFKD(c4) == NFKD(c5) + assert_equal_codepoints col5, @proxy.new(col1).normalize(:kd), "Form KD - Col 5 has to be NFKD(1) - #{comment}" + assert_equal_codepoints col5, @proxy.new(col2).normalize(:kd), "Form KD - Col 5 has to be NFKD(2) - #{comment}" + assert_equal_codepoints col5, @proxy.new(col3).normalize(:kd), "Form KD - Col 5 has to be NFKD(3) - #{comment}" + assert_equal_codepoints col5, @proxy.new(col4).normalize(:kd), "Form KD - Col 5 has to be NFKD(4) - #{comment}" + assert_equal_codepoints col5, @proxy.new(col5).normalize(:kd), "Form KD - Col 5 has to be NFKD(5) - #{comment}" + end end end diff --git a/activesupport/test/multibyte_test_helpers.rb b/activesupport/test/multibyte_test_helpers.rb index f7cf993100..7565655f25 100644 --- a/activesupport/test/multibyte_test_helpers.rb +++ b/activesupport/test/multibyte_test_helpers.rb @@ -1,5 +1,9 @@ # frozen_string_literal: true +require "fileutils" +require "open-uri" +require "tmpdir" + module MultibyteTestHelpers class Downloader def self.download(from, to) @@ -23,9 +27,9 @@ module MultibyteTestHelpers CACHE_DIR = "#{Dir.tmpdir}/cache/unicode_conformance/#{ActiveSupport::Multibyte::Unicode::UNICODE_VERSION}" FileUtils.mkdir_p(CACHE_DIR) - UNICODE_STRING = "こにちわ".freeze - ASCII_STRING = "ohayo".freeze - BYTE_STRING = "\270\236\010\210\245".dup.force_encoding("ASCII-8BIT").freeze + UNICODE_STRING = "こにちわ" + ASCII_STRING = "ohayo" + BYTE_STRING = (+"\270\236\010\210\245").force_encoding("ASCII-8BIT").freeze def chars(str) ActiveSupport::Multibyte::Chars.new(str) diff --git a/activesupport/test/multibyte_unicode_database_test.rb b/activesupport/test/multibyte_unicode_database_test.rb deleted file mode 100644 index 540a34493d..0000000000 --- a/activesupport/test/multibyte_unicode_database_test.rb +++ /dev/null @@ -1,26 +0,0 @@ -# frozen_string_literal: true - -require "abstract_unit" - -class MultibyteUnicodeDatabaseTest < ActiveSupport::TestCase - include ActiveSupport::Multibyte::Unicode - - def setup - @ucd = UnicodeDatabase.new - end - - UnicodeDatabase::ATTRIBUTES.each do |attribute| - define_method "test_lazy_loading_on_attribute_access_of_#{attribute}" do - assert_called(@ucd, :load) do - @ucd.send(attribute) - end - end - end - - def test_load - @ucd.load - UnicodeDatabase::ATTRIBUTES.each do |attribute| - assert @ucd.send(attribute).length > 1 - end - end -end 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 1d76c91d30..d5c9e82e9f 100644 --- a/activesupport/test/notifications/instrumenter_test.rb +++ b/activesupport/test/notifications/instrumenter_test.rb @@ -47,13 +47,13 @@ module ActiveSupport def test_start instrumenter.start("foo", payload) assert_equal [["foo", instrumenter.id, payload]], notifier.starts - assert_predicate notifier.finishes, :empty? + assert_empty notifier.finishes end def test_finish instrumenter.finish("foo", payload) assert_equal [["foo", instrumenter.id, payload]], notifier.finishes - assert_predicate notifier.starts, :empty? + assert_empty notifier.starts end end end diff --git a/activesupport/test/notifications_test.rb b/activesupport/test/notifications_test.rb index 5cfbd60131..bb20d26a25 100644 --- a/activesupport/test/notifications_test.rb +++ b/activesupport/test/notifications_test.rb @@ -26,6 +26,42 @@ module Notifications end end + class SubscribeEventObjectsTest < TestCase + def test_subscribe_events + events = [] + @notifier.subscribe do |event| + events << event + end + + ActiveSupport::Notifications.instrument("foo") + event = events.first + assert event, "should have an event" + assert_operator event.allocations, :>, 0 + assert_operator event.cpu_time, :>, 0 + assert_operator event.idle_time, :>, 0 + assert_operator event.duration, :>, 0 + end + + def test_subscribe_via_top_level_api + old_notifier = ActiveSupport::Notifications.notifier + ActiveSupport::Notifications.notifier = ActiveSupport::Notifications::Fanout.new + + event = nil + ActiveSupport::Notifications.subscribe("foo") do |e| + event = e + end + + ActiveSupport::Notifications.instrument("foo") do + 100.times { Object.new } # allocate at least 100 objects + end + + assert event + assert_operator event.allocations, :>=, 100 + ensure + ActiveSupport::Notifications.notifier = old_notifier + end + end + class SubscribedTest < TestCase def test_subscribed name = "foo" @@ -45,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 @@ -54,7 +90,7 @@ module Notifications ActiveSupport::Notifications.subscribe("foo", TestSubscriber.new) ActiveSupport::Notifications.instrument("foo") do - ActiveSupport::Notifications.subscribe("foo") {} + ActiveSupport::Notifications.subscribe("foo") { } end ensure ActiveSupport::Notifications.notifier = old_notifier @@ -92,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 @@ -250,8 +305,8 @@ module Notifications time = Time.now event = event(:foo, time, time + 0.01, random_id, {}) - assert_equal :foo, event.name - assert_equal time, event.time + assert_equal :foo, event.name + assert_equal time, event.time assert_in_delta 10.0, event.duration, 0.00001 end @@ -270,9 +325,9 @@ module Notifications parent.children << child assert parent.parent_of?(child) - assert !child.parent_of?(parent) - assert !parent.parent_of?(not_child) - assert !not_child.parent_of?(parent) + assert_not child.parent_of?(parent) + assert_not parent.parent_of?(not_child) + assert_not not_child.parent_of?(parent) end private diff --git a/activesupport/test/ordered_options_test.rb b/activesupport/test/ordered_options_test.rb index 2c67bb02ac..90394fee0a 100644 --- a/activesupport/test/ordered_options_test.rb +++ b/activesupport/test/ordered_options_test.rb @@ -15,7 +15,7 @@ class OrderedOptionsTest < ActiveSupport::TestCase a[:allow_concurrency] = false assert_equal 1, a.size - assert !a[:allow_concurrency] + assert_not a[:allow_concurrency] a["else_where"] = 56 assert_equal 2, a.size @@ -47,7 +47,7 @@ class OrderedOptionsTest < ActiveSupport::TestCase a.allow_concurrency = false assert_equal 1, a.size - assert !a.allow_concurrency + assert_not a.allow_concurrency a.else_where = 56 assert_equal 2, a.size @@ -82,8 +82,8 @@ class OrderedOptionsTest < ActiveSupport::TestCase def test_introspection a = ActiveSupport::OrderedOptions.new - assert a.respond_to?(:blah) - assert a.respond_to?(:blah=) + assert_respond_to a, :blah + assert_respond_to a, :blah= assert_equal 42, a.method(:blah=).call(42) assert_equal 42, a.method(:blah).call end @@ -91,7 +91,7 @@ class OrderedOptionsTest < ActiveSupport::TestCase def test_raises_with_bang a = ActiveSupport::OrderedOptions.new a[:foo] = :bar - assert a.respond_to?(:foo!) + assert_respond_to a, :foo! assert_nothing_raised { a.foo! } assert_equal a.foo, a.foo! diff --git a/activesupport/test/parameter_filter_test.rb b/activesupport/test/parameter_filter_test.rb new file mode 100644 index 0000000000..d2dc71061d --- /dev/null +++ b/activesupport/test/parameter_filter_test.rb @@ -0,0 +1,105 @@ +# frozen_string_literal: true + +require "abstract_unit" +require "active_support/core_ext/hash" +require "active_support/parameter_filter" + +class ParameterFilterTest < ActiveSupport::TestCase + test "process parameter filter" do + test_hashes = [ + [{ "foo" => "bar" }, { "foo" => "bar" }, %w'food'], + [{ "foo" => "bar" }, { "foo" => "[FILTERED]" }, %w'foo'], + [{ "foo" => "bar", "bar" => "foo" }, { "foo" => "[FILTERED]", "bar" => "foo" }, %w'foo baz'], + [{ "foo" => "bar", "baz" => "foo" }, { "foo" => "[FILTERED]", "baz" => "[FILTERED]" }, %w'foo baz'], + [{ "bar" => { "foo" => "bar", "bar" => "foo" } }, { "bar" => { "foo" => "[FILTERED]", "bar" => "foo" } }, %w'fo'], + [{ "foo" => { "foo" => "bar", "bar" => "foo" } }, { "foo" => "[FILTERED]" }, %w'f banana'], + [{ "deep" => { "cc" => { "code" => "bar", "bar" => "foo" }, "ss" => { "code" => "bar" } } }, { "deep" => { "cc" => { "code" => "[FILTERED]", "bar" => "foo" }, "ss" => { "code" => "bar" } } }, %w'deep.cc.code'], + [{ "baz" => [{ "foo" => "baz" }, "1"] }, { "baz" => [{ "foo" => "[FILTERED]" }, "1"] }, [/foo/]]] + + test_hashes.each do |before_filter, after_filter, filter_words| + parameter_filter = ActiveSupport::ParameterFilter.new(filter_words) + assert_equal after_filter, parameter_filter.filter(before_filter) + + filter_words << "blah" + filter_words << lambda { |key, value| + value.reverse! if key =~ /bargain/ + } + filter_words << lambda { |key, value, original_params| + value.replace("world!") if original_params["barg"]["blah"] == "bar" && key == "hello" + } + + parameter_filter = ActiveSupport::ParameterFilter.new(filter_words) + before_filter["barg"] = { :bargain => "gain", "blah" => "bar", "bar" => { "bargain" => { "blah" => "foo", "hello" => "world" } } } + after_filter["barg"] = { :bargain => "niag", "blah" => "[FILTERED]", "bar" => { "bargain" => { "blah" => "[FILTERED]", "hello" => "world!" } } } + + assert_equal after_filter, parameter_filter.filter(before_filter) + end + end + + test "filter should return mask option when value is filtered" do + mask = Object.new.freeze + test_hashes = [ + [{ "foo" => "bar" }, { "foo" => "bar" }, %w'food'], + [{ "foo" => "bar" }, { "foo" => mask }, %w'foo'], + [{ "foo" => "bar", "bar" => "foo" }, { "foo" => mask, "bar" => "foo" }, %w'foo baz'], + [{ "foo" => "bar", "baz" => "foo" }, { "foo" => mask, "baz" => mask }, %w'foo baz'], + [{ "bar" => { "foo" => "bar", "bar" => "foo" } }, { "bar" => { "foo" => mask, "bar" => "foo" } }, %w'fo'], + [{ "foo" => { "foo" => "bar", "bar" => "foo" } }, { "foo" => mask }, %w'f banana'], + [{ "deep" => { "cc" => { "code" => "bar", "bar" => "foo" }, "ss" => { "code" => "bar" } } }, { "deep" => { "cc" => { "code" => mask, "bar" => "foo" }, "ss" => { "code" => "bar" } } }, %w'deep.cc.code'], + [{ "baz" => [{ "foo" => "baz" }, "1"] }, { "baz" => [{ "foo" => mask }, "1"] }, [/foo/]]] + + test_hashes.each do |before_filter, after_filter, filter_words| + parameter_filter = ActiveSupport::ParameterFilter.new(filter_words, mask: mask) + assert_equal after_filter, parameter_filter.filter(before_filter) + + filter_words << "blah" + filter_words << lambda { |key, value| + value.reverse! if key =~ /bargain/ + } + filter_words << lambda { |key, value, original_params| + value.replace("world!") if original_params["barg"]["blah"] == "bar" && key == "hello" + } + + parameter_filter = ActiveSupport::ParameterFilter.new(filter_words, mask: mask) + before_filter["barg"] = { :bargain => "gain", "blah" => "bar", "bar" => { "bargain" => { "blah" => "foo", "hello" => "world" } } } + after_filter["barg"] = { :bargain => "niag", "blah" => mask, "bar" => { "bargain" => { "blah" => mask, "hello" => "world!" } } } + + assert_equal after_filter, parameter_filter.filter(before_filter) + end + end + + test "filter_param" do + parameter_filter = ActiveSupport::ParameterFilter.new(["foo", /bar/]) + assert_equal "[FILTERED]", parameter_filter.filter_param("food", "secret vlaue") + assert_equal "[FILTERED]", parameter_filter.filter_param("baz.foo", "secret vlaue") + assert_equal "[FILTERED]", parameter_filter.filter_param("barbar", "secret vlaue") + assert_equal "non secret value", parameter_filter.filter_param("baz", "non secret value") + end + + test "filter_param can work with empty filters" do + parameter_filter = ActiveSupport::ParameterFilter.new + assert_equal "bar", parameter_filter.filter_param("foo", "bar") + end + + test "parameter filter should maintain hash with indifferent access" do + test_hashes = [ + [{ "foo" => "bar" }.with_indifferent_access, ["blah"]], + [{ "foo" => "bar" }.with_indifferent_access, []] + ] + + test_hashes.each do |before_filter, filter_words| + parameter_filter = ActiveSupport::ParameterFilter.new(filter_words) + assert_instance_of ActiveSupport::HashWithIndifferentAccess, + parameter_filter.filter(before_filter) + end + end + + test "filter_param should return mask option when value is filtered" do + mask = Object.new.freeze + parameter_filter = ActiveSupport::ParameterFilter.new(["foo", /bar/], mask: mask) + assert_equal mask, parameter_filter.filter_param("food", "secret vlaue") + assert_equal mask, parameter_filter.filter_param("baz.foo", "secret vlaue") + assert_equal mask, parameter_filter.filter_param("barbar", "secret vlaue") + assert_equal "non secret value", parameter_filter.filter_param("baz", "non secret value") + end +end diff --git a/activesupport/test/reloader_test.rb b/activesupport/test/reloader_test.rb index 3e4229eaf7..1b7cc253d9 100644 --- a/activesupport/test/reloader_test.rb +++ b/activesupport/test/reloader_test.rb @@ -8,18 +8,18 @@ class ReloaderTest < ActiveSupport::TestCase reloader.to_prepare { prepared = true } reloader.to_complete { completed = true } - assert !prepared - assert !completed + assert_not prepared + assert_not completed reloader.prepare! assert prepared - assert !completed + assert_not completed prepared = false reloader.wrap do assert prepared prepared = false end - assert !prepared + assert_not prepared end def test_prepend_prepare_callback @@ -35,14 +35,14 @@ class ReloaderTest < ActiveSupport::TestCase r = new_reloader { true } invoked = false r.to_run { invoked = true } - r.wrap {} + r.wrap { } assert invoked r = new_reloader { false } invoked = false r.to_run { invoked = true } - r.wrap {} - assert !invoked + r.wrap { } + assert_not invoked end def test_full_reload_sequence @@ -53,7 +53,7 @@ class ReloaderTest < ActiveSupport::TestCase reloader.executor.to_run { called << :executor_run } reloader.executor.to_complete { called << :executor_complete } - reloader.wrap {} + reloader.wrap { } assert_equal [:executor_run, :reloader_run, :prepare, :reloader_complete, :executor_complete], called called = [] @@ -63,7 +63,7 @@ class ReloaderTest < ActiveSupport::TestCase reloader.check = lambda { false } called = [] - reloader.wrap {} + reloader.wrap { } assert_equal [:executor_run, :executor_complete], called called = [] diff --git a/activesupport/test/safe_buffer_test.rb b/activesupport/test/safe_buffer_test.rb index 05c2fb59be..08d04e3223 100644 --- a/activesupport/test/safe_buffer_test.rb +++ b/activesupport/test/safe_buffer_test.rb @@ -39,7 +39,7 @@ class SafeBufferTest < ActiveSupport::TestCase end test "Should be considered safe" do - assert @buffer.html_safe? + assert_predicate @buffer, :html_safe? end test "Should return a safe buffer when calling to_s" do @@ -75,16 +75,41 @@ class SafeBufferTest < ActiveSupport::TestCase assert_equal "my_test", str end - test "Should not return safe buffer from gsub" do - altered_buffer = @buffer.gsub("", "asdf") - assert_equal "asdf", altered_buffer - assert !altered_buffer.html_safe? - end + { + capitalize: nil, + chomp: nil, + chop: nil, + delete: "foo", + delete_prefix: "foo", + delete_suffix: "foo", + downcase: nil, + gsub: ["foo", "bar"], + lstrip: nil, + next: nil, + reverse: nil, + rstrip: nil, + slice: "foo", + squeeze: nil, + strip: nil, + sub: ["foo", "bar"], + succ: nil, + swapcase: nil, + tr: ["foo", "bar"], + tr_s: ["foo", "bar"], + unicode_normalize: nil, + upcase: nil, + }.each do |unsafe_method, dummy_args| + test "Should not return safe buffer from #{unsafe_method}" do + skip unless String.method_defined?(unsafe_method) + altered_buffer = @buffer.send(unsafe_method, *dummy_args) + assert_not_predicate altered_buffer, :html_safe? + end - test "Should not return safe buffer from gsub!" do - @buffer.gsub!("", "asdf") - assert_equal "asdf", @buffer - assert !@buffer.html_safe? + test "Should not return safe buffer from #{unsafe_method}!" do + skip unless String.method_defined?("#{unsafe_method}!") + @buffer.send("#{unsafe_method}!", *dummy_args) + assert_not_predicate @buffer, :html_safe? + end end test "Should escape dirty buffers on add" do @@ -101,13 +126,13 @@ class SafeBufferTest < ActiveSupport::TestCase test "Should preserve html_safe? status on copy" do @buffer.gsub!("", "<>") - assert !@buffer.dup.html_safe? + assert_not_predicate @buffer.dup, :html_safe? end test "Should return safe buffer when added with another safe buffer" do clean = "<script>".html_safe result_buffer = @buffer + clean - assert result_buffer.html_safe? + assert_predicate result_buffer, :html_safe? assert_equal "<script>", result_buffer end @@ -126,9 +151,9 @@ class SafeBufferTest < ActiveSupport::TestCase assert_equal "", ActiveSupport::SafeBuffer.new("foo").clone_empty end - test "clone_empty keeps the original dirtyness" do - assert @buffer.clone_empty.html_safe? - assert !@buffer.gsub!("", "").clone_empty.html_safe? + 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 test "Should be safe when sliced if original value was safe" do @@ -141,13 +166,25 @@ class SafeBufferTest < ActiveSupport::TestCase x = "foo".html_safe.gsub!("f", '<script>alert("lolpwnd");</script>') # calling gsub! makes the dirty flag true - assert !x.html_safe?, "should not be safe" + assert_not x.html_safe?, "should not be safe" # getting a slice of it y = x[0..-1] # should still be unsafe - assert !y.html_safe?, "should not be safe" + assert_not y.html_safe?, "should not be safe" + end + + test "Should continue safe on slice" do + x = "<div>foo</div>".html_safe + + assert_predicate x, :html_safe? + + # getting a slice of it + y = x[0..-1] + + # should still be safe + assert_predicate y, :html_safe? end test "Should work with interpolation (array argument)" do diff --git a/activesupport/test/security_utils_test.rb b/activesupport/test/security_utils_test.rb index 0a607594a2..fff9cc2a8d 100644 --- a/activesupport/test/security_utils_test.rb +++ b/activesupport/test/security_utils_test.rb @@ -11,7 +11,7 @@ class SecurityUtilsTest < ActiveSupport::TestCase def test_fixed_length_secure_compare_should_perform_string_comparison assert ActiveSupport::SecurityUtils.fixed_length_secure_compare("a", "a") - assert !ActiveSupport::SecurityUtils.fixed_length_secure_compare("a", "b") + assert_not ActiveSupport::SecurityUtils.fixed_length_secure_compare("a", "b") end def test_fixed_length_secure_compare_raise_on_length_mismatch diff --git a/activesupport/test/share_lock_test.rb b/activesupport/test/share_lock_test.rb index 42fd5eefc1..a40c813fe3 100644 --- a/activesupport/test/share_lock_test.rb +++ b/activesupport/test/share_lock_test.rb @@ -11,38 +11,38 @@ class ShareLockTest < ActiveSupport::TestCase def test_reentrancy thread = Thread.new do - @lock.sharing { @lock.sharing {} } - @lock.exclusive { @lock.exclusive {} } + @lock.sharing { @lock.sharing { } } + @lock.exclusive { @lock.exclusive { } } end assert_threads_not_stuck thread end def test_sharing_doesnt_block with_thread_waiting_in_lock_section(:sharing) do |sharing_thread_latch| - assert_threads_not_stuck(Thread.new { @lock.sharing {} }) + assert_threads_not_stuck(Thread.new { @lock.sharing { } }) end end def test_sharing_blocks_exclusive with_thread_waiting_in_lock_section(:sharing) do |sharing_thread_release_latch| @lock.exclusive(no_wait: true) { flunk } # polling should fail - exclusive_thread = Thread.new { @lock.exclusive {} } + exclusive_thread = Thread.new { @lock.exclusive { } } assert_threads_stuck_but_releasable_by_latch exclusive_thread, sharing_thread_release_latch end end def test_exclusive_blocks_sharing with_thread_waiting_in_lock_section(:exclusive) do |exclusive_thread_release_latch| - sharing_thread = Thread.new { @lock.sharing {} } + sharing_thread = Thread.new { @lock.sharing { } } assert_threads_stuck_but_releasable_by_latch sharing_thread, exclusive_thread_release_latch 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 - @lock.exclusive {} + @lock.exclusive { } end end @@ -53,7 +53,7 @@ class ShareLockTest < ActiveSupport::TestCase def test_sharing_is_upgradeable_to_exclusive upgrading_thread = Thread.new do @lock.sharing do - @lock.exclusive {} + @lock.exclusive { } end end assert_threads_not_stuck upgrading_thread @@ -66,7 +66,7 @@ class ShareLockTest < ActiveSupport::TestCase upgrading_thread = Thread.new do @lock.sharing do in_sharing.count_down - @lock.exclusive {} + @lock.exclusive { } end end @@ -81,7 +81,7 @@ class ShareLockTest < ActiveSupport::TestCase exclusive_threads = (1..2).map do Thread.new do @lock.send(use_upgrading ? :sharing : :tap) do - @lock.exclusive(purpose: :load, compatible: [:load, :unload]) {} + @lock.exclusive(purpose: :load, compatible: [:load, :unload]) { } end end end @@ -95,7 +95,7 @@ class ShareLockTest < ActiveSupport::TestCase with_thread_waiting_in_lock_section(:sharing) do |sharing_thread_release_latch| thread = Thread.new do @lock.sharing do - @lock.exclusive {} + @lock.exclusive { } end end @@ -105,7 +105,7 @@ class ShareLockTest < ActiveSupport::TestCase sharing_thread_release_latch.count_down thread = Thread.new do - @lock.exclusive {} + @lock.exclusive { } end assert_threads_not_stuck thread @@ -115,68 +115,66 @@ class ShareLockTest < ActiveSupport::TestCase def test_exclusive_conflicting_purpose [true, false].each do |use_upgrading| with_thread_waiting_in_lock_section(:sharing) do |sharing_thread_release_latch| - begin - together = Concurrent::CyclicBarrier.new(2) - conflicting_exclusive_threads = [ - Thread.new do - @lock.send(use_upgrading ? :sharing : :tap) do - together.wait - @lock.exclusive(purpose: :red, compatible: [:green, :purple]) {} - end - end, - Thread.new do - @lock.send(use_upgrading ? :sharing : :tap) do - together.wait - @lock.exclusive(purpose: :blue, compatible: [:green]) {} - end + together = Concurrent::CyclicBarrier.new(2) + conflicting_exclusive_threads = [ + Thread.new do + @lock.send(use_upgrading ? :sharing : :tap) do + together.wait + @lock.exclusive(purpose: :red, compatible: [:green, :purple]) { } + end + end, + Thread.new do + @lock.send(use_upgrading ? :sharing : :tap) do + together.wait + @lock.exclusive(purpose: :blue, compatible: [:green]) { } end - ] - - assert_threads_stuck conflicting_exclusive_threads # wait for threads to get into their respective `exclusive {}` blocks - - # This thread will be stuck as long as any other thread is in - # a sharing block. While it's blocked, it holds no lock, so it - # doesn't interfere with any other attempts. - no_purpose_thread = Thread.new do - @lock.exclusive {} end - assert_threads_stuck no_purpose_thread + ] - # This thread is compatible with both of the "primary" - # attempts above. It's initially stuck on the outer share - # lock, but as soon as that's released, it can run -- - # regardless of whether those threads hold share locks. - compatible_thread = Thread.new do - @lock.exclusive(purpose: :green, compatible: []) {} - end - assert_threads_stuck compatible_thread + assert_threads_stuck conflicting_exclusive_threads # wait for threads to get into their respective `exclusive {}` blocks - assert_threads_stuck conflicting_exclusive_threads + # This thread will be stuck as long as any other thread is in + # a sharing block. While it's blocked, it holds no lock, so it + # doesn't interfere with any other attempts. + no_purpose_thread = Thread.new do + @lock.exclusive { } + end + assert_threads_stuck no_purpose_thread + + # This thread is compatible with both of the "primary" + # attempts above. It's initially stuck on the outer share + # lock, but as soon as that's released, it can run -- + # regardless of whether those threads hold share locks. + compatible_thread = Thread.new do + @lock.exclusive(purpose: :green, compatible: []) { } + end + assert_threads_stuck compatible_thread - sharing_thread_release_latch.count_down + assert_threads_stuck conflicting_exclusive_threads - assert_threads_not_stuck compatible_thread # compatible thread is now able to squeak through + sharing_thread_release_latch.count_down - if use_upgrading - # The "primary" threads both each hold a share lock, and are - # mutually incompatible; they're still stuck. - assert_threads_stuck conflicting_exclusive_threads + assert_threads_not_stuck compatible_thread # compatible thread is now able to squeak through - # The thread without a specified purpose is also stuck; it's - # not compatible with anything. - assert_threads_stuck no_purpose_thread - else - # As the primaries didn't hold a share lock, as soon as the - # outer one was released, all the exclusive locks are free - # to be acquired in turn. + if use_upgrading + # The "primary" threads both each hold a share lock, and are + # mutually incompatible; they're still stuck. + assert_threads_stuck conflicting_exclusive_threads - assert_threads_not_stuck conflicting_exclusive_threads - assert_threads_not_stuck no_purpose_thread - end - ensure - conflicting_exclusive_threads.each(&:kill) - no_purpose_thread.kill + # The thread without a specified purpose is also stuck; it's + # not compatible with anything. + assert_threads_stuck no_purpose_thread + else + # As the primaries didn't hold a share lock, as soon as the + # outer one was released, all the exclusive locks are free + # to be acquired in turn. + + assert_threads_not_stuck conflicting_exclusive_threads + assert_threads_not_stuck no_purpose_thread end + ensure + conflicting_exclusive_threads.each(&:kill) + no_purpose_thread.kill end end end @@ -231,7 +229,7 @@ class ShareLockTest < ActiveSupport::TestCase assert_threads_stuck waiting_exclusive late_share_attempt = Thread.new do - @lock.sharing {} + @lock.sharing { } end assert_threads_stuck late_share_attempt @@ -252,14 +250,14 @@ class ShareLockTest < ActiveSupport::TestCase @lock.sharing do ready.wait attempt_reentrancy.wait - @lock.sharing {} + @lock.sharing { } end end exclusive = Thread.new do @lock.sharing do ready.wait - @lock.exclusive {} + @lock.exclusive { } end end @@ -280,7 +278,7 @@ class ShareLockTest < ActiveSupport::TestCase Thread.new do @lock.sharing do ready.wait - @lock.exclusive(purpose: :x, compatible: [:x], after_compatible: [:x]) {} + @lock.exclusive(purpose: :x, compatible: [:x], after_compatible: [:x]) { } done.wait end end @@ -297,7 +295,7 @@ class ShareLockTest < ActiveSupport::TestCase Thread.new do @lock.sharing do ready.wait - @lock.exclusive(purpose: :x) {} + @lock.exclusive(purpose: :x) { } done.wait end end, @@ -323,7 +321,7 @@ class ShareLockTest < ActiveSupport::TestCase Thread.new do @lock.sharing do ready.wait - @lock.exclusive(purpose: :x) {} + @lock.exclusive(purpose: :x) { } done.wait end end, @@ -352,7 +350,7 @@ class ShareLockTest < ActiveSupport::TestCase Thread.new do @lock.sharing do ready.wait - @lock.exclusive(purpose: :x) {} + @lock.exclusive(purpose: :x) { } done.wait end end, @@ -386,7 +384,7 @@ class ShareLockTest < ActiveSupport::TestCase incompatible_thread = Thread.new do @lock.sharing do ready.wait - @lock.exclusive(purpose: :x) {} + @lock.exclusive(purpose: :x) { } end end @@ -418,7 +416,7 @@ class ShareLockTest < ActiveSupport::TestCase incompatible_thread = Thread.new do ready.wait - @lock.exclusive(purpose: :z) {} + @lock.exclusive(purpose: :z) { } end recursive_yield_shares_thread = Thread.new do @@ -427,7 +425,7 @@ class ShareLockTest < ActiveSupport::TestCase @lock.yield_shares(compatible: [:y]) do do_nesting.wait @lock.sharing do - @lock.yield_shares(compatible: [:x, :y]) {} + @lock.yield_shares(compatible: [:x, :y]) { } end after_nesting.wait end @@ -439,12 +437,12 @@ class ShareLockTest < ActiveSupport::TestCase assert_threads_stuck incompatible_thread compatible_thread = Thread.new do - @lock.exclusive(purpose: :y) {} + @lock.exclusive(purpose: :y) { } end assert_threads_not_stuck compatible_thread post_nesting_incompatible_thread = Thread.new do - @lock.exclusive(purpose: :x) {} + @lock.exclusive(purpose: :x) { } end assert_threads_stuck post_nesting_incompatible_thread diff --git a/activesupport/test/silence_logger_test.rb b/activesupport/test/silence_logger_test.rb new file mode 100644 index 0000000000..bd0c6b7f86 --- /dev/null +++ b/activesupport/test/silence_logger_test.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require "abstract_unit" +require "active_support/logger_silence" +require "logger" + +class LoggerSilenceTest < ActiveSupport::TestCase + class MyLogger < ::Logger + include ActiveSupport::LoggerSilence + end + + setup do + @io = StringIO.new + @logger = MyLogger.new(@io) + end + + test "#silence silences the log" do + @logger.silence(Logger::ERROR) do + @logger.info("Foo") + end + @io.rewind + + assert_empty @io.read + end + + test "#debug? is true when setting the temporary level to Logger::DEBUG" do + @logger.level = Logger::INFO + + @logger.silence(Logger::DEBUG) do + assert_predicate @logger, :debug? + end + + assert_predicate @logger, :info? + end +end diff --git a/activesupport/test/string_inquirer_test.rb b/activesupport/test/string_inquirer_test.rb index bf8f878a32..09726b144a 100644 --- a/activesupport/test/string_inquirer_test.rb +++ b/activesupport/test/string_inquirer_test.rb @@ -8,11 +8,11 @@ class StringInquirerTest < ActiveSupport::TestCase end def test_match - assert @string_inquirer.production? + assert_predicate @string_inquirer, :production? end def test_miss - assert_not @string_inquirer.development? + assert_not_predicate @string_inquirer, :development? end def test_missing_question_mark diff --git a/activesupport/test/tagged_logging_test.rb b/activesupport/test/tagged_logging_test.rb index 5fd6a6b316..cff73472c3 100644 --- a/activesupport/test/tagged_logging_test.rb +++ b/activesupport/test/tagged_logging_test.rb @@ -19,9 +19,10 @@ class TaggedLoggingTest < ActiveSupport::TestCase test "sets logger.formatter if missing and extends it with a tagging API" do logger = Logger.new(StringIO.new) assert_nil logger.formatter - ActiveSupport::TaggedLogging.new(logger) - assert_not_nil logger.formatter - assert logger.formatter.respond_to?(:tagged) + + other_logger = ActiveSupport::TaggedLogging.new(logger) + assert_not_nil other_logger.formatter + assert_respond_to other_logger.formatter, :tagged end test "tagged once" do @@ -74,24 +75,37 @@ class TaggedLoggingTest < ActiveSupport::TestCase test "keeps each tag in their own thread" do @logger.tagged("BCX") do Thread.new do + @logger.info "Dull story" @logger.tagged("OMG") { @logger.info "Cool story" } end.join @logger.info "Funky time" end - assert_equal "[OMG] Cool story\n[BCX] Funky time\n", @output.string + assert_equal "Dull story\n[OMG] Cool story\n[BCX] Funky time\n", @output.string end test "keeps each tag in their own instance" do - @other_output = StringIO.new - @other_logger = ActiveSupport::TaggedLogging.new(MyLogger.new(@other_output)) + other_output = StringIO.new + other_logger = ActiveSupport::TaggedLogging.new(MyLogger.new(other_output)) @logger.tagged("OMG") do - @other_logger.tagged("BCX") do + other_logger.tagged("BCX") do @logger.info "Cool story" - @other_logger.info "Funky time" + other_logger.info "Funky time" end end assert_equal "[OMG] Cool story\n", @output.string - assert_equal "[BCX] Funky time\n", @other_output.string + assert_equal "[BCX] Funky time\n", other_output.string + end + + test "does not share the same formatter instance of the original logger" do + other_logger = ActiveSupport::TaggedLogging.new(@logger) + + @logger.tagged("OMG") do + other_logger.tagged("BCX") do + @logger.info "Cool story" + other_logger.info "Funky time" + end + end + assert_equal "[OMG] Cool story\n[BCX] Funky time\n", @output.string end test "cleans up the taggings on flush" do diff --git a/activesupport/test/test_case_test.rb b/activesupport/test/test_case_test.rb index 84e4953fe2..56cd2665e0 100644 --- a/activesupport/test/test_case_test.rb +++ b/activesupport/test/test_case_test.rb @@ -2,7 +2,7 @@ require "abstract_unit" -class AssertDifferenceTest < ActiveSupport::TestCase +class AssertionsTest < ActiveSupport::TestCase def setup @object = Class.new do attr_accessor :num @@ -52,6 +52,22 @@ class AssertDifferenceTest < ActiveSupport::TestCase assert_equal "Object Changed.\n\"@object.num\" didn't change by 0.\nExpected: 0\n Actual: 1", error.message end + def test_assert_no_difference_with_multiple_expressions_pass + another_object = @object.dup + assert_no_difference ["@object.num", -> { another_object.num }] do + # ... + end + end + + def test_assert_no_difference_with_multiple_expressions_fail + another_object = @object.dup + assert_raises(Minitest::Assertion) do + assert_no_difference ["@object.num", -> { another_object.num }], "Another Object Changed" do + another_object.increment + end + end + end + def test_assert_difference assert_difference "@object.num", +1 do @object.increment @@ -88,7 +104,7 @@ class AssertDifferenceTest < ActiveSupport::TestCase def test_expression_is_evaluated_in_the_appropriate_scope silence_warnings do local_scope = "foo" - local_scope = local_scope # to suppress unused variable warning + _ = local_scope # to suppress unused variable warning assert_difference("local_scope; @object.num") { @object.increment } end end @@ -115,6 +131,35 @@ class AssertDifferenceTest < ActiveSupport::TestCase end end + def test_hash_of_expressions + assert_difference "@object.num" => 1, "@object.num + 1" => 1 do + @object.increment + end + end + + def test_hash_of_expressions_with_message + error = assert_raises Minitest::Assertion do + assert_difference({ "@object.num" => 0 }, "Object Changed") do + @object.increment + end + end + assert_equal "Object Changed.\n\"@object.num\" didn't change by 0.\nExpected: 0\n Actual: 1", error.message + end + + def test_hash_of_lambda_expressions + assert_difference -> { @object.num } => 1, -> { @object.num + 1 } => 1 do + @object.increment + end + end + + def test_hash_of_expressions_identify_failure + assert_raises(Minitest::Assertion) do + assert_difference "@object.num" => 1, "1 + 1" => 1 do + @object.increment + end + end + end + def test_assert_changes_pass assert_changes "@object.num" do @object.increment @@ -156,6 +201,16 @@ class AssertDifferenceTest < ActiveSupport::TestCase end end + def test_assert_changes_with_to_option_but_no_change_has_special_message + error = assert_raises Minitest::Assertion do + assert_changes "@object.num", to: 0 do + # no changes + end + end + + assert_equal "\"@object.num\" didn't change. It was already 0", error.message + end + def test_assert_changes_with_wrong_to_option assert_raises Minitest::Assertion do assert_changes "@object.num", to: 2 do @@ -218,10 +273,11 @@ class AssertDifferenceTest < ActiveSupport::TestCase def test_assert_changes_with_message error = assert_raises Minitest::Assertion do assert_changes "@object.num", "@object.num should 1", to: 1 do + @object.decrement end end - assert_equal "@object.num should 1.\n\"@object.num\" didn't change to 1", error.message + assert_equal "@object.num should 1.\n\"@object.num\" didn't change to as expected\nExpected: 1\n Actual: -1", error.message end def test_assert_no_changes_pass diff --git a/activesupport/test/testing/after_teardown_test.rb b/activesupport/test/testing/after_teardown_test.rb new file mode 100644 index 0000000000..961af49479 --- /dev/null +++ b/activesupport/test/testing/after_teardown_test.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +require "abstract_unit" + +module OtherAfterTeardown + def after_teardown + super + + @witness = true + end +end + +class AfterTeardownTest < ActiveSupport::TestCase + include OtherAfterTeardown + + attr_writer :witness + + MyError = Class.new(StandardError) + + teardown do + raise MyError, "Test raises an error, all after_teardown should still get called" + end + + def after_teardown + assert_changes -> { failures.count }, from: 0, to: 1 do + super + end + + assert_equal true, @witness + failures.clear + end + + def test_teardown_raise_but_all_after_teardown_method_are_called + assert true + end +end diff --git a/activesupport/test/testing/method_call_assertions_test.rb b/activesupport/test/testing/method_call_assertions_test.rb index 4af000bb7e..669463bd31 100644 --- a/activesupport/test/testing/method_call_assertions_test.rb +++ b/activesupport/test/testing/method_call_assertions_test.rb @@ -1,11 +1,8 @@ # frozen_string_literal: true require "abstract_unit" -require "active_support/testing/method_call_assertions" class MethodCallAssertionsTest < ActiveSupport::TestCase - include ActiveSupport::Testing::MethodCallAssertions - class Level def increment; 1; end def decrement; end @@ -39,6 +36,8 @@ class MethodCallAssertionsTest < ActiveSupport::TestCase assert_called(@object, :increment, returns: 10) do assert_equal 10, @object.increment end + + assert_equal 1, @object.increment end def test_assert_called_failure @@ -61,18 +60,20 @@ class MethodCallAssertionsTest < ActiveSupport::TestCase assert_match(/dang it.\nExpected increment/, error.message) end - def test_assert_called_with - assert_called_with(@object, :increment) do - @object.increment - end - end - def test_assert_called_with_arguments assert_called_with(@object, :<<, [ 2 ]) do @object << 2 end end + def test_assert_called_with_arguments_and_returns + assert_called_with(@object, :<<, [ 2 ], returns: 10) do + assert_equal(10, @object << 2) + end + + assert_nil(@object << 2) + end + def test_assert_called_with_failure assert_raises(MockExpectationError) do assert_called_with(@object, :<<, [ 4567 ]) do @@ -81,19 +82,72 @@ class MethodCallAssertionsTest < ActiveSupport::TestCase end end - def test_assert_called_with_returns - assert_called_with(@object, :increment, returns: 1) do + def test_assert_called_with_multiple_expected_arguments + assert_called_with(@object, :<<, [ [ 1 ], [ 2 ] ]) do + @object << 1 + @object << 2 + end + end + + def test_assert_called_on_instance_of_with_defaults_to_expect_once + assert_called_on_instance_of Level, :increment do @object.increment end end - def test_assert_called_with_multiple_expected_arguments - assert_called_with(@object, :<<, [ [ 1 ], [ 2 ] ]) do - @object << 1 + def test_assert_called_on_instance_of_more_than_once + assert_called_on_instance_of(Level, :increment, times: 2) do + @object.increment + @object.increment + end + end + + def test_assert_called_on_instance_of_with_arguments + assert_called_on_instance_of(Level, :<<) do @object << 2 end end + def test_assert_called_on_instance_of_returns + assert_called_on_instance_of(Level, :increment, returns: 10) do + assert_equal 10, @object.increment + end + + assert_equal 1, @object.increment + end + + def test_assert_called_on_instance_of_failure + error = assert_raises(Minitest::Assertion) do + assert_called_on_instance_of(Level, :increment) do + # Call nothing... + end + end + + assert_equal "Expected increment to be called 1 times, but was called 0 times.\nExpected: 1\n Actual: 0", error.message + end + + def test_assert_called_on_instance_of_with_message + error = assert_raises(Minitest::Assertion) do + assert_called_on_instance_of(Level, :increment, "dang it") do + # Call nothing... + end + end + + assert_match(/dang it.\nExpected increment/, error.message) + end + + def test_assert_called_on_instance_of_nesting + assert_called_on_instance_of(Level, :increment, times: 3) do + assert_called_on_instance_of(Level, :decrement, times: 2) do + @object.increment + @object.decrement + @object.increment + @object.decrement + @object.increment + end + end + end + def test_assert_not_called assert_not_called(@object, :decrement) do @object.increment @@ -110,6 +164,30 @@ class MethodCallAssertionsTest < ActiveSupport::TestCase assert_equal "Expected increment to be called 0 times, but was called 1 times.\nExpected: 0\n Actual: 1", error.message end + def test_assert_not_called_on_instance_of + assert_not_called_on_instance_of(Level, :decrement) do + @object.increment + end + end + + def test_assert_not_called_on_instance_of_failure + error = assert_raises(Minitest::Assertion) do + assert_not_called_on_instance_of(Level, :increment) do + @object.increment + end + end + + assert_equal "Expected increment to be called 0 times, but was called 1 times.\nExpected: 0\n Actual: 1", error.message + end + + def test_assert_not_called_on_instance_of_nesting + assert_not_called_on_instance_of(Level, :increment) do + assert_not_called_on_instance_of(Level, :decrement) do + # Call nothing... + end + end + end + def test_stub_any_instance stub_any_instance(Level) do |instance| assert_equal instance, Level.new diff --git a/activesupport/test/time_travel_test.rb b/activesupport/test/time_travel_test.rb index 9c2c635f43..a1f84bf69e 100644 --- a/activesupport/test/time_travel_test.rb +++ b/activesupport/test/time_travel_test.rb @@ -3,24 +3,25 @@ require "abstract_unit" require "active_support/core_ext/date_time" require "active_support/core_ext/numeric/time" +require "time_zone_test_helpers" class TimeTravelTest < ActiveSupport::TestCase + include TimeZoneTestHelpers + class TimeSubclass < ::Time; end class DateSubclass < ::Date; end class DateTimeSubclass < ::DateTime; end def test_time_helper_travel Time.stub(:now, Time.now) do - begin - expected_time = Time.now + 1.day - travel 1.day + expected_time = Time.now + 1.day + travel 1.day - assert_equal expected_time.to_s(:db), Time.now.to_s(:db) - assert_equal expected_time.to_date, Date.today - assert_equal expected_time.to_datetime.to_s(:db), DateTime.now.to_s(:db) - ensure - travel_back - end + assert_equal expected_time.to_s(:db), Time.now.to_s(:db) + assert_equal expected_time.to_date, Date.today + assert_equal expected_time.to_datetime.to_s(:db), DateTime.now.to_s(:db) + ensure + travel_back end end @@ -42,16 +43,14 @@ class TimeTravelTest < ActiveSupport::TestCase def test_time_helper_travel_to Time.stub(:now, Time.now) do - begin - expected_time = Time.new(2004, 11, 24, 01, 04, 44) - travel_to expected_time + expected_time = Time.new(2004, 11, 24, 01, 04, 44) + travel_to expected_time - assert_equal expected_time, Time.now - assert_equal Date.new(2004, 11, 24), Date.today - assert_equal expected_time.to_datetime, DateTime.now - ensure - travel_back - end + assert_equal expected_time, Time.now + assert_equal Date.new(2004, 11, 24), Date.today + assert_equal expected_time.to_datetime, DateTime.now + ensure + travel_back end end @@ -71,23 +70,35 @@ class TimeTravelTest < ActiveSupport::TestCase end end + def test_time_helper_travel_to_with_time_zone + with_env_tz "US/Eastern" do + with_tz_default ActiveSupport::TimeZone["UTC"] do + Time.stub(:now, Time.now) do + expected_time = 5.minutes.ago + + travel_to 5.minutes.ago do + assert_equal expected_time.to_s(:db), Time.zone.now.to_s(:db) + end + end + end + end + end + def test_time_helper_travel_back Time.stub(:now, Time.now) do - begin - expected_time = Time.new(2004, 11, 24, 01, 04, 44) + expected_time = Time.new(2004, 11, 24, 01, 04, 44) - travel_to expected_time - assert_equal expected_time, Time.now - assert_equal Date.new(2004, 11, 24), Date.today - assert_equal expected_time.to_datetime, DateTime.now - travel_back + travel_to expected_time + assert_equal expected_time, Time.now + assert_equal Date.new(2004, 11, 24), Date.today + assert_equal expected_time.to_datetime, DateTime.now + travel_back - assert_not_equal expected_time, Time.now - assert_not_equal Date.new(2004, 11, 24), Date.today - assert_not_equal expected_time.to_datetime, DateTime.now - ensure - travel_back - end + assert_not_equal expected_time, Time.now + assert_not_equal Date.new(2004, 11, 24), Date.today + assert_not_equal expected_time.to_datetime, DateTime.now + ensure + travel_back end end @@ -122,24 +133,22 @@ class TimeTravelTest < ActiveSupport::TestCase def test_time_helper_travel_to_with_subsequent_calls Time.stub(:now, Time.now) do - begin - initial_expected_time = Time.new(2004, 11, 24, 01, 04, 44) - subsequent_expected_time = Time.new(2004, 10, 24, 01, 04, 44) - assert_nothing_raised do - travel_to initial_expected_time - travel_to subsequent_expected_time + initial_expected_time = Time.new(2004, 11, 24, 01, 04, 44) + subsequent_expected_time = Time.new(2004, 10, 24, 01, 04, 44) + assert_nothing_raised do + travel_to initial_expected_time + travel_to subsequent_expected_time - assert_equal subsequent_expected_time, Time.now + assert_equal subsequent_expected_time, Time.now - travel_back - end - ensure travel_back end + ensure + travel_back 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 @@ -186,4 +195,8 @@ class TimeTravelTest < ActiveSupport::TestCase assert_operator expected_time.to_s(:db), :<, Time.now.to_s(:db) end + + def test_time_helper_unfreeze_time + assert_equal method(:travel_back), method(:unfreeze_time) + end end diff --git a/activesupport/test/time_zone_test.rb b/activesupport/test/time_zone_test.rb index 405c8f315b..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)) @@ -225,6 +225,16 @@ class TimeZoneTest < ActiveSupport::TestCase assert_equal secs, twz.to_f end + def test_at_with_microseconds + zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"] + secs = 946684800.0 + microsecs = 123456.789 + twz = zone.at(secs, microsecs) + assert_equal zone, twz.time_zone + assert_equal secs, twz.to_i + assert_equal 123456789, twz.nsec + end + def test_iso8601 zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"] twz = zone.iso8601("1999-12-31T19:00:00") @@ -725,6 +735,21 @@ class TimeZoneTest < ActiveSupport::TestCase assert_not_includes all_zones, galapagos end + def test_all_doesnt_raise_exception_with_missing_tzinfo_data + mappings = { + "Puerto Rico" => "America/Unknown", + "Pittsburgh" => "America/New_York" + } + + with_tz_mappings(mappings) do + assert_nil ActiveSupport::TimeZone["Puerto Rico"] + assert_nil ActiveSupport::TimeZone[-9] + assert_nothing_raised do + ActiveSupport::TimeZone.all + end + end + end + def test_index assert_nil ActiveSupport::TimeZone["bogus"] assert_instance_of ActiveSupport::TimeZone, ActiveSupport::TimeZone["Central Time (US & Canada)"] @@ -756,6 +781,16 @@ class TimeZoneTest < ActiveSupport::TestCase assert_not_includes ActiveSupport::TimeZone.country_zones(:ru), ActiveSupport::TimeZone["Kuala Lumpur"] end + def test_country_zones_with_and_without_mappings + assert_includes ActiveSupport::TimeZone.country_zones("au"), ActiveSupport::TimeZone["Adelaide"] + assert_includes ActiveSupport::TimeZone.country_zones("au"), ActiveSupport::TimeZone["Australia/Lord_Howe"] + end + + def test_country_zones_with_multiple_mappings + assert_includes ActiveSupport::TimeZone.country_zones("gb"), ActiveSupport::TimeZone["Edinburgh"] + assert_includes ActiveSupport::TimeZone.country_zones("gb"), ActiveSupport::TimeZone["London"] + end + def test_country_zones_without_mappings assert_includes ActiveSupport::TimeZone.country_zones(:sv), ActiveSupport::TimeZone["America/El_Salvador"] end diff --git a/activesupport/test/time_zone_test_helpers.rb b/activesupport/test/time_zone_test_helpers.rb index 051703a781..85ed727c9b 100644 --- a/activesupport/test/time_zone_test_helpers.rb +++ b/activesupport/test/time_zone_test_helpers.rb @@ -23,4 +23,17 @@ module TimeZoneTestHelpers ensure ActiveSupport.to_time_preserves_timezone = old_preserve_tz end + + def with_tz_mappings(mappings) + old_mappings = ActiveSupport::TimeZone::MAPPING.dup + ActiveSupport::TimeZone.clear + ActiveSupport::TimeZone::MAPPING.clear + ActiveSupport::TimeZone::MAPPING.merge!(mappings) + + yield + ensure + ActiveSupport::TimeZone.clear + ActiveSupport::TimeZone::MAPPING.clear + ActiveSupport::TimeZone::MAPPING.merge!(old_mappings) + end end diff --git a/activesupport/test/xml_mini/rexml_engine_test.rb b/activesupport/test/xml_mini/rexml_engine_test.rb index 34bf81fa75..b711619ba7 100644 --- a/activesupport/test/xml_mini/rexml_engine_test.rb +++ b/activesupport/test/xml_mini/rexml_engine_test.rb @@ -12,7 +12,7 @@ class REXMLEngineTest < XMLMiniEngineTest end def test_parse_from_frozen_string - xml_string = "<root></root>".freeze + xml_string = "<root></root>" assert_equal({ "root" => {} }, ActiveSupport::XmlMini.parse(xml_string)) end diff --git a/activesupport/test/xml_mini/xml_mini_engine_test.rb b/activesupport/test/xml_mini/xml_mini_engine_test.rb index 5c4c28d9b7..c62e7e32c9 100644 --- a/activesupport/test/xml_mini/xml_mini_engine_test.rb +++ b/activesupport/test/xml_mini/xml_mini_engine_test.rb @@ -78,7 +78,7 @@ class XMLMiniEngineTest < ActiveSupport::TestCase end def test_parse_from_frozen_string - xml_string = "<root/>".freeze + xml_string = "<root/>" assert_equal({ "root" => {} }, ActiveSupport::XmlMini.parse(xml_string)) end |