diff options
Diffstat (limited to 'activesupport')
46 files changed, 1024 insertions, 243 deletions
diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md index 1f459a843d..bd333da081 100644 --- a/activesupport/CHANGELOG.md +++ b/activesupport/CHANGELOG.md @@ -1,3 +1,43 @@ +* Fix regression in `Hash#dig` for HashWithIndifferentAccess. + *Jon Moss* + +## Rails 5.0.0.beta2 (February 01, 2016) ## + +* Change number_to_currency behavior for checking negativity. + + Used `to_f.negative` instead of using `to_f.phase` for checking negativity + of a number in number_to_currency helper. + This change works same for all cases except when number is "-0.0". + + -0.0.to_f.negative? => false + -0.0.to_f.phase? => 3.14 + + This change reverts changes from https://github.com/rails/rails/pull/6512. + But it should be acceptable as we could not find any currency which + supports negative zeros. + + *Prathamesh Sonpatki*, *Rafael Mendonça França* + +* Match `HashWithIndifferentAccess#default`'s behaviour with `Hash#default`. + + *David Cornu* + +* Adds `:exception_object` key to `ActiveSupport::Notifications::Instrumenter` + payload when an exception is raised. + + Adds new key/value pair to payload when an exception is raised: + e.g. `:exception_object => #<RuntimeError: FAIL>`. + + *Ryan T. Hosford* + +* Support extended grapheme clusters and UAX 29. + + *Adam Roben* + +* Add petabyte and exabyte numeric conversion. + + *Akshay Vishnoi* + ## Rails 5.0.0.beta1 (December 18, 2015) ## * Add thread_m/cattr_accessor/reader/writer suite of methods for declaring class and module variables that live per-thread. @@ -7,44 +47,44 @@ Here's an example of a simple event tracking system where the object being tracked needs not pass a creator that it doesn't need itself along: - module Current - thread_mattr_accessor :account - thread_mattr_accessor :user - - def self.reset() self.account = self.user = nil end - end - - class ApplicationController < ActiveController::Base - before_action :set_current - after_action { Current.reset } - - private - def set_current - Current.account = Account.find(params[:account_id]) - Current.user = Current.account.users.find(params[:user_id]) + module Current + thread_mattr_accessor :account + thread_mattr_accessor :user + + def self.reset() self.account = self.user = nil end end - end - class MessagesController < ApplicationController - def create - @message = Message.create!(message_params) - end - end - - class Message < ApplicationRecord - has_many :events - after_create :track_created - - private - def track_created - events.create! origin: self, action: :create + class ApplicationController < ActionController::Base + before_action :set_current + after_action { Current.reset } + + private + def set_current + Current.account = Account.find(params[:account_id]) + Current.user = Current.account.users.find(params[:user_id]) + end + end + + class MessagesController < ApplicationController + def create + @message = Message.create!(message_params) + end end - end - class Event < ApplicationRecord - belongs_to :creator, class_name: 'User' - before_validation { self.creator ||= Current.user } - end + class Message < ApplicationRecord + has_many :events + after_create :track_created + + private + def track_created + events.create! origin: self, action: :create + end + end + + class Event < ApplicationRecord + belongs_to :creator, class_name: 'User' + before_validation { self.creator ||= Current.user } + end *DHH* @@ -58,9 +98,9 @@ *Yuichiro Kaneko* -* `ActiveSupport::Cache::Store#namespaced_key`, - `ActiveSupport::Cache::MemCachedStore#escape_key`, and - `ActiveSupport::Cache::FileStore#key_file_path` +* `ActiveSupport::Cache::Store#namespaced_key`, + `ActiveSupport::Cache::MemCachedStore#escape_key`, and + `ActiveSupport::Cache::FileStore#key_file_path` are deprecated and replaced with `normalize_key` that now calls `super`. `ActiveSupport::Cache::LocaleCache#set_cache_value` is deprecated and replaced with `write_cache_value`. @@ -119,7 +159,7 @@ *Konstantinos Rousis* -* Handle invalid UTF-8 strings when HTML escaping +* Handle invalid UTF-8 strings when HTML escaping. Use `ActiveSupport::Multibyte::Unicode.tidy_bytes` to handle invalid UTF-8 strings in `ERB::Util.unwrapped_html_escape` and `ERB::Util.html_escape_once`. @@ -160,7 +200,7 @@ * Short-circuit `blank?` on date and time values since they are never blank. - Fixes #21657 + Fixes #21657. *Andrew White* @@ -198,7 +238,7 @@ * ActiveSupport::HashWithIndifferentAccess `select` and `reject` will now return enumerator if called without block. - Fixes #20095 + Fixes #20095. *Bernard Potocki* @@ -212,11 +252,11 @@ *Simon Eskildsen* -* Fix setting `default_proc` on `HashWithIndifferentAccess#dup` +* Fix setting `default_proc` on `HashWithIndifferentAccess#dup`. *Simon Eskildsen* -* Fix a range of values for parameters of the Time#change +* Fix a range of values for parameters of the Time#change. *Nikolay Kondratyev* @@ -228,7 +268,7 @@ *Kevin Deisz* * Add a bang version to `ActiveSupport::OrderedOptions` get methods which will raise - an `KeyError` if the value is `.blank?` + an `KeyError` if the value is `.blank?`. Before: diff --git a/activesupport/MIT-LICENSE b/activesupport/MIT-LICENSE index 7bffebb076..40235833ba 100644 --- a/activesupport/MIT-LICENSE +++ b/activesupport/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2005-2015 David Heinemeier Hansson +Copyright (c) 2005-2016 David Heinemeier Hansson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the @@ -17,4 +17,4 @@ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
\ No newline at end of file +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/activesupport/Rakefile b/activesupport/Rakefile index 81c242d4b1..33ee62aa1b 100644 --- a/activesupport/Rakefile +++ b/activesupport/Rakefile @@ -1,6 +1,10 @@ require 'rake/testtask' task :default => :test + +task :package +task "package:clean" + Rake::TestTask.new do |t| t.libs << 'test' t.pattern = 'test/**/*_test.rb' diff --git a/activesupport/activesupport.gemspec b/activesupport/activesupport.gemspec index 32e28c0212..68a80701ed 100644 --- a/activesupport/activesupport.gemspec +++ b/activesupport/activesupport.gemspec @@ -21,9 +21,7 @@ Gem::Specification.new do |s| s.rdoc_options.concat ['--encoding', 'UTF-8'] s.add_dependency 'i18n', '~> 0.7' - s.add_dependency 'json', '~> 1.7', '>= 1.7.7' s.add_dependency 'tzinfo', '~> 1.1' s.add_dependency 'minitest', '~> 5.1' s.add_dependency 'concurrent-ruby', '~> 1.0' - s.add_dependency 'method_source' end diff --git a/activesupport/lib/active_support.rb b/activesupport/lib/active_support.rb index 2019afeb00..94fe893149 100644 --- a/activesupport/lib/active_support.rb +++ b/activesupport/lib/active_support.rb @@ -1,5 +1,5 @@ #-- -# Copyright (c) 2005-2015 David Heinemeier Hansson +# Copyright (c) 2005-2016 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/cache/file_store.rb b/activesupport/lib/active_support/cache/file_store.rb index dff2443bc8..99c55b1aa4 100644 --- a/activesupport/lib/active_support/cache/file_store.rb +++ b/activesupport/lib/active_support/cache/file_store.rb @@ -17,6 +17,7 @@ module ActiveSupport 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) super(options) @@ -24,10 +25,10 @@ module ActiveSupport end # Deletes all items from the cache. In this case it deletes all the entries in the specified - # file store directory except for .gitkeep. Be careful which directory is specified in your + # 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 = Dir.entries(cache_path).reject {|f| (EXCLUDED_DIRS + [".gitkeep"]).include?(f)} + root_dirs = exclude_from(cache_path, EXCLUDED_DIRS + GITKEEP_FILES) FileUtils.rm_r(root_dirs.collect{|f| File.join(cache_path, f)}) rescue Errno::ENOENT end @@ -154,7 +155,7 @@ module ActiveSupport # Delete empty directories in the cache. def delete_empty_directories(dir) return if File.realpath(dir) == File.realpath(cache_path) - if Dir.entries(dir).reject {|f| EXCLUDED_DIRS.include?(f)}.empty? + if exclude_from(dir, EXCLUDED_DIRS).empty? Dir.delete(dir) rescue nil delete_empty_directories(File.dirname(dir)) end @@ -193,6 +194,11 @@ 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/callbacks.rb b/activesupport/lib/active_support/callbacks.rb index bf560ec1fa..e6baddf5db 100644 --- a/activesupport/lib/active_support/callbacks.rb +++ b/activesupport/lib/active_support/callbacks.rb @@ -71,7 +71,7 @@ module ActiveSupport # halt the entire callback chain and display a deprecation message. # If false, callback chains will only be halted by calling +throw :abort+. # Defaults to +true+. - mattr_accessor(:halt_and_display_warning_on_return_false) { true } + mattr_accessor(:halt_and_display_warning_on_return_false, instance_writer: false) { true } # Runs the callbacks for the given event. # @@ -742,7 +742,7 @@ module ActiveSupport options = names.extract_options! names.each do |name| - class_attribute "_#{name}_callbacks" + class_attribute "_#{name}_callbacks", instance_writer: false set_callbacks name, CallbackChain.new(name, options) module_eval <<-RUBY, __FILE__, __LINE__ + 1 diff --git a/activesupport/lib/active_support/concurrency/share_lock.rb b/activesupport/lib/active_support/concurrency/share_lock.rb index ca48164c54..54244317e4 100644 --- a/activesupport/lib/active_support/concurrency/share_lock.rb +++ b/activesupport/lib/active_support/concurrency/share_lock.rb @@ -6,12 +6,6 @@ module ActiveSupport # A share/exclusive lock, otherwise known as a read/write lock. # # https://en.wikipedia.org/wiki/Readers%E2%80%93writer_lock - #-- - # Note that a pending Exclusive lock attempt does not block incoming - # Share requests (i.e., we are "read-preferring"). That seems - # consistent with the behavior of "loose" upgrades, but may be the - # wrong choice otherwise: it nominally reduces the possibility of - # deadlock by risking starvation instead. class ShareLock include MonitorMixin @@ -48,17 +42,11 @@ module ActiveSupport def start_exclusive(purpose: nil, compatible: [], no_wait: false) synchronize do unless @exclusive_thread == Thread.current - if busy?(purpose) + if busy_for_exclusive?(purpose) return false if no_wait - loose_shares = @sharing.delete(Thread.current) - @waiting[Thread.current] = compatible if loose_shares - - begin - @cv.wait_while { busy?(purpose) } - ensure - @waiting.delete Thread.current - @sharing[Thread.current] = loose_shares if loose_shares + yield_shares(purpose: purpose, compatible: compatible, block_share: true) do + @cv.wait_while { busy_for_exclusive?(purpose) } end end @exclusive_thread = Thread.current @@ -71,13 +59,19 @@ module ActiveSupport # Relinquish the exclusive lock. Must only be called by the thread # that called start_exclusive (and currently holds the lock). - def stop_exclusive + def stop_exclusive(compatible: []) synchronize do raise "invalid unlock" if @exclusive_thread != Thread.current @exclusive_depth -= 1 if @exclusive_depth == 0 @exclusive_thread = nil + + if eligible_waiters?(compatible) + yield_shares(compatible: compatible, block_share: true) do + @cv.wait_while { @exclusive_thread || eligible_waiters?(compatible) } + end + end @cv.broadcast end end @@ -85,8 +79,16 @@ module ActiveSupport def start_sharing synchronize do - if @exclusive_thread && @exclusive_thread != Thread.current + if @sharing[Thread.current] > 0 || @exclusive_thread == Thread.current + # We already hold a lock; nothing to wait for + elsif @waiting[Thread.current] + # We're nested inside a +yield_shares+ call: we'll resume as + # soon as there isn't an exclusive lock in our way @cv.wait_while { @exclusive_thread } + else + # This is an initial / outermost share call: any outstanding + # requests for an exclusive lock get to go first + @cv.wait_while { busy_for_sharing?(false) } end @sharing[Thread.current] += 1 end @@ -109,12 +111,12 @@ module ActiveSupport # the block. # # See +start_exclusive+ for other options. - def exclusive(purpose: nil, compatible: [], no_wait: false) + def exclusive(purpose: nil, compatible: [], after_compatible: [], no_wait: false) if start_exclusive(purpose: purpose, compatible: compatible, no_wait: no_wait) begin yield ensure - stop_exclusive + stop_exclusive(compatible: after_compatible) end end end @@ -129,14 +131,56 @@ module ActiveSupport end end + # Temporarily give up all held Share locks while executing the + # supplied block, allowing any +compatible+ exclusive lock request + # to proceed. + def yield_shares(purpose: nil, compatible: [], block_share: false) + loose_shares = previous_wait = nil + synchronize do + if loose_shares = @sharing.delete(Thread.current) + if previous_wait = @waiting[Thread.current] + purpose = nil unless purpose == previous_wait[0] + compatible &= previous_wait[1] + end + compatible |= [false] unless block_share + @waiting[Thread.current] = [purpose, compatible] + + @cv.broadcast + end + end + + begin + yield + ensure + synchronize do + @cv.wait_while { @exclusive_thread && @exclusive_thread != Thread.current } + + if previous_wait + @waiting[Thread.current] = previous_wait + else + @waiting.delete Thread.current + end + @sharing[Thread.current] = loose_shares if loose_shares + end + end + end + private # Must be called within synchronize - def busy?(purpose) - (@exclusive_thread && @exclusive_thread != Thread.current) || - @waiting.any? { |k, v| k != Thread.current && !v.include?(purpose) } || + def busy_for_exclusive?(purpose) + busy_for_sharing?(purpose) || @sharing.size > (@sharing[Thread.current] > 0 ? 1 : 0) end + + def busy_for_sharing?(purpose) + (@exclusive_thread && @exclusive_thread != Thread.current) || + @waiting.any? { |t, (_, c)| t != Thread.current && !c.include?(purpose) } + end + + def eligible_waiters?(compatible) + @waiting.any? { |t, (p, _)| compatible.include?(p) && @waiting.all? { |t2, (_, c2)| t == t2 || c2.include?(p) } } + end end end end diff --git a/activesupport/lib/active_support/core_ext/hash/conversions.rb b/activesupport/lib/active_support/core_ext/hash/conversions.rb index 8594d9bf2e..6741e732f0 100644 --- a/activesupport/lib/active_support/core_ext/hash/conversions.rb +++ b/activesupport/lib/active_support/core_ext/hash/conversions.rb @@ -138,6 +138,8 @@ end module ActiveSupport class XMLConverter # :nodoc: + # Raised if the XML contains attributes with type="yaml" or + # type="symbol". Read Hash#from_xml for more details. class DisallowedType < StandardError def initialize(type) super "Disallowed type attribute: #{type.inspect}" diff --git a/activesupport/lib/active_support/core_ext/kernel/concern.rb b/activesupport/lib/active_support/core_ext/kernel/concern.rb index bf72caa058..18bcc01fa4 100644 --- a/activesupport/lib/active_support/core_ext/kernel/concern.rb +++ b/activesupport/lib/active_support/core_ext/kernel/concern.rb @@ -1,6 +1,8 @@ require 'active_support/core_ext/module/concerning' module Kernel + module_function + # A shortcut to define a toplevel concern, not within a module. # # See Module::Concerning for more. diff --git a/activesupport/lib/active_support/core_ext/kernel/reporting.rb b/activesupport/lib/active_support/core_ext/kernel/reporting.rb index 8afc258df8..d0197af95f 100644 --- a/activesupport/lib/active_support/core_ext/kernel/reporting.rb +++ b/activesupport/lib/active_support/core_ext/kernel/reporting.rb @@ -1,4 +1,6 @@ module Kernel + module_function + # Sets $VERBOSE to nil for the duration of the block and back to its original # value afterwards. # diff --git a/activesupport/lib/active_support/core_ext/module/deprecation.rb b/activesupport/lib/active_support/core_ext/module/deprecation.rb index 56d670fbe8..f3f2e7f5fc 100644 --- a/activesupport/lib/active_support/core_ext/module/deprecation.rb +++ b/activesupport/lib/active_support/core_ext/module/deprecation.rb @@ -13,8 +13,8 @@ class Module # # class MyLib::Deprecator # def deprecation_warning(deprecated_method_name, message, caller_backtrace = nil) - # message = "#{deprecated_method_name} is deprecated and will be removed from MyLibrary | #{message}" - # Kernel.warn message + # message = "#{deprecated_method_name} is deprecated and will be removed from MyLibrary | #{message}" + # Kernel.warn message # end # end def deprecate(*method_names) diff --git a/activesupport/lib/active_support/core_ext/numeric/conversions.rb b/activesupport/lib/active_support/core_ext/numeric/conversions.rb index 9a3651f29a..9d832897ed 100644 --- a/activesupport/lib/active_support/core_ext/numeric/conversions.rb +++ b/activesupport/lib/active_support/core_ext/numeric/conversions.rb @@ -1,5 +1,6 @@ require 'active_support/core_ext/big_decimal/conversions' require 'active_support/number_helper' +require 'active_support/core_ext/module/deprecation' module ActiveSupport::NumericWithFormat @@ -75,6 +76,8 @@ module ActiveSupport::NumericWithFormat # 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 @@ -117,7 +120,11 @@ module ActiveSupport::NumericWithFormat when :human_size return ActiveSupport::NumberHelper.number_to_human_size(self, options) else - super + if is_a?(Float) || format.is_a?(Symbol) + super() + else + super + end end end 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 04ed8e7cd8..43b9fd4bf7 100644 --- a/activesupport/lib/active_support/core_ext/string/output_safety.rb +++ b/activesupport/lib/active_support/core_ext/string/output_safety.rb @@ -141,6 +141,7 @@ module ActiveSupport #:nodoc: alias_method :original_concat, :concat private :original_concat + # Raised when <tt>ActiveSupport::SafeBuffer#safe_concat</tt> is called on unsafe buffers. class SafeConcatError < StandardError def initialize super 'Could not concatenate to the buffer because it is not html safe.' @@ -170,7 +171,7 @@ module ActiveSupport #:nodoc: original_concat(value) end - def initialize(*) + def initialize(str = '') @html_safe = true super end diff --git a/activesupport/lib/active_support/core_ext/time/zones.rb b/activesupport/lib/active_support/core_ext/time/zones.rb index 877dc84ec8..7a60f94996 100644 --- a/activesupport/lib/active_support/core_ext/time/zones.rb +++ b/activesupport/lib/active_support/core_ext/time/zones.rb @@ -40,7 +40,23 @@ class Time Thread.current[:time_zone] = find_zone!(time_zone) end - # Allows override of <tt>Time.zone</tt> locally inside supplied block; resets <tt>Time.zone</tt> to existing value when done. + # Allows override of <tt>Time.zone</tt> locally inside supplied block; + # resets <tt>Time.zone</tt> to existing value when done. + # + # class ApplicationController < ActionController::Base + # around_action :set_time_zone + # + # private + # + # def set_time_zone + # Time.use_zone(current_user.timezone) { yield } + # end + # end + # + # NOTE: This won't affect any <tt>ActiveSupport::TimeWithZone</tt> + # objects that have already been created, e.g. any model timestamp + # attributes that have been read before the block will remain in + # the application's default timezone. def use_zone(time_zone) new_zone = find_zone!(time_zone) begin diff --git a/activesupport/lib/active_support/dependencies/interlock.rb b/activesupport/lib/active_support/dependencies/interlock.rb index fbeb904684..47bcecbb35 100644 --- a/activesupport/lib/active_support/dependencies/interlock.rb +++ b/activesupport/lib/active_support/dependencies/interlock.rb @@ -8,13 +8,13 @@ module ActiveSupport #:nodoc: end def loading - @lock.exclusive(purpose: :load, compatible: [:load]) do + @lock.exclusive(purpose: :load, compatible: [:load], after_compatible: [:load]) do yield end end def unloading - @lock.exclusive(purpose: :unload, compatible: [:load, :unload]) do + @lock.exclusive(purpose: :unload, compatible: [:load, :unload], after_compatible: [:load, :unload]) do yield end end @@ -24,7 +24,7 @@ module ActiveSupport #:nodoc: # concurrent activity, return immediately (without executing the # block) instead of waiting. def attempt_unloading - @lock.exclusive(purpose: :unload, compatible: [:load, :unload], no_wait: true) do + @lock.exclusive(purpose: :unload, compatible: [:load, :unload], after_compatible: [:load, :unload], no_wait: true) do yield end end @@ -42,6 +42,12 @@ module ActiveSupport #:nodoc: yield end end + + def permit_concurrent_loads + @lock.yield_shares(compatible: [:load]) do + yield + end + end end end end diff --git a/activesupport/lib/active_support/deprecation/behaviors.rb b/activesupport/lib/active_support/deprecation/behaviors.rb index 28d2d78643..0de891f1a2 100644 --- a/activesupport/lib/active_support/deprecation/behaviors.rb +++ b/activesupport/lib/active_support/deprecation/behaviors.rb @@ -1,6 +1,8 @@ require "active_support/notifications" module ActiveSupport + # Raised when <tt>ActiveSupport::Deprecation::Behavior#behavior</tt> is set with <tt>:raise</tt>. + # You would set <tt>:raise</tt>, as a behaviour to raise errors and proactively report exceptions from deprecations. class DeprecationException < StandardError end diff --git a/activesupport/lib/active_support/deprecation/method_wrappers.rb b/activesupport/lib/active_support/deprecation/method_wrappers.rb index 32fe8025fe..f5ea6669ce 100644 --- a/activesupport/lib/active_support/deprecation/method_wrappers.rb +++ b/activesupport/lib/active_support/deprecation/method_wrappers.rb @@ -21,15 +21,15 @@ module ActiveSupport # # => [:aaa, :bbb, :ccc] # # Fred.aaa - # # DEPRECATION WARNING: aaa is deprecated and will be removed from Rails 5.0. (called from irb_binding at (irb):10) + # # DEPRECATION WARNING: aaa is deprecated and will be removed from Rails 5.1. (called from irb_binding at (irb):10) # # => nil # # Fred.bbb - # # DEPRECATION WARNING: bbb is deprecated and will be removed from Rails 5.0 (use zzz instead). (called from irb_binding at (irb):11) + # # DEPRECATION WARNING: bbb is deprecated and will be removed from Rails 5.1 (use zzz instead). (called from irb_binding at (irb):11) # # => nil # # Fred.ccc - # # DEPRECATION WARNING: ccc is deprecated and will be removed from Rails 5.0 (use Bar#ccc instead). (called from irb_binding at (irb):12) + # # DEPRECATION WARNING: ccc is deprecated and will be removed from Rails 5.1 (use Bar#ccc instead). (called from irb_binding at (irb):12) # # => nil # # Passing in a custom deprecator: diff --git a/activesupport/lib/active_support/deprecation/proxy_wrappers.rb b/activesupport/lib/active_support/deprecation/proxy_wrappers.rb index 6f0ad445fc..0cb2d4d22e 100644 --- a/activesupport/lib/active_support/deprecation/proxy_wrappers.rb +++ b/activesupport/lib/active_support/deprecation/proxy_wrappers.rb @@ -80,7 +80,7 @@ module ActiveSupport # example.old_request.to_s # # => DEPRECATION WARNING: @request is deprecated! Call request.to_s instead of # @request.to_s - # (Bactrace information…) + # (Backtrace information…) # "special_request" # # example.request.to_s @@ -118,7 +118,7 @@ module ActiveSupport # # PLANETS.map { |planet| planet.capitalize } # # => DEPRECATION WARNING: PLANETS is deprecated! Use PLANETS_POST_2006 instead. - # (Bactrace information…) + # (Backtrace information…) # ["Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune"] class DeprecatedConstantProxy < DeprecationProxy def initialize(old_const, new_const, deprecator = ActiveSupport::Deprecation.instance) diff --git a/activesupport/lib/active_support/gem_version.rb b/activesupport/lib/active_support/gem_version.rb index 7790a9b2c0..fc08273b6d 100644 --- a/activesupport/lib/active_support/gem_version.rb +++ b/activesupport/lib/active_support/gem_version.rb @@ -8,7 +8,7 @@ module ActiveSupport MAJOR = 5 MINOR = 0 TINY = 0 - PRE = "beta1" + PRE = "beta2" 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 4ff35a45a1..03770a197c 100644 --- a/activesupport/lib/active_support/hash_with_indifferent_access.rb +++ b/activesupport/lib/active_support/hash_with_indifferent_access.rb @@ -68,8 +68,10 @@ module ActiveSupport end end - def default(key = nil) - if key.is_a?(Symbol) && include?(key = key.to_s) + def default(*args) + arg_key = args.first + + if include?(key = convert_key(arg_key)) self[key] else super @@ -159,6 +161,20 @@ module ActiveSupport alias_method :has_key?, :key? alias_method :member?, :key? + + # Same as <tt>Hash#[]</tt> where the key passed as argument can be + # either a string or a symbol: + # + # counters = ActiveSupport::HashWithIndifferentAccess.new + # counters[:foo] = 1 + # + # counters['foo'] # => 1 + # counters[:foo] # => 1 + # counters[:zoo] # => nil + def [](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: # diff --git a/activesupport/lib/active_support/inflector/methods.rb b/activesupport/lib/active_support/inflector/methods.rb index 595b0339cc..f741c0bfb8 100644 --- a/activesupport/lib/active_support/inflector/methods.rb +++ b/activesupport/lib/active_support/inflector/methods.rb @@ -173,7 +173,7 @@ module ActiveSupport # # Singular names are not handled correctly: # - # classify('calculus') # => "Calculu" + # classify('calculus') # => "Calculus" def classify(table_name) # strip out any leading schema name camelize(singularize(table_name.to_s.sub(/.*\./, ''.freeze))) diff --git a/activesupport/lib/active_support/locale/en.yml b/activesupport/lib/active_support/locale/en.yml index a4563ace8f..c64b7598ee 100644 --- a/activesupport/lib/active_support/locale/en.yml +++ b/activesupport/lib/active_support/locale/en.yml @@ -106,6 +106,8 @@ en: mb: "MB" gb: "GB" tb: "TB" + pb: "PB" + eb: "EB" # Used in NumberHelper.number_to_human() decimal_units: format: "%n %u" diff --git a/activesupport/lib/active_support/logger.rb b/activesupport/lib/active_support/logger.rb index 82117a64d2..7626b28108 100644 --- a/activesupport/lib/active_support/logger.rb +++ b/activesupport/lib/active_support/logger.rb @@ -5,18 +5,27 @@ module ActiveSupport class Logger < ::Logger include LoggerSilence - attr_accessor :broadcast_messages + # Returns true if the logger destination matches one of the sources + # + # logger = Logger.new(STDOUT) + # ActiveSupport::Logger.logger_outputs_to?(logger, STDOUT) + # # => true + def self.logger_outputs_to?(logger, *sources) + logdev = logger.instance_variable_get("@logdev") + logger_source = logdev.dev if logdev.respond_to?(:dev) + sources.any? { |source| source == logger_source } + end # Broadcasts logs to multiple loggers. def self.broadcast(logger) # :nodoc: Module.new do define_method(:add) do |*args, &block| - logger.add(*args, &block) if broadcast_messages + logger.add(*args, &block) super(*args, &block) end define_method(:<<) do |x| - logger << x if broadcast_messages + logger << x super(x) end @@ -45,7 +54,20 @@ module ActiveSupport def initialize(*args) super @formatter = SimpleFormatter.new - @broadcast_messages = true + 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 7d92256f24..125d81d973 100644 --- a/activesupport/lib/active_support/logger_silence.rb +++ b/activesupport/lib/active_support/logger_silence.rb @@ -1,25 +1,45 @@ require 'active_support/concern' require 'active_support/core_ext/module/attribute_accessors' +require 'concurrent' module LoggerSilence extend ActiveSupport::Concern included do cattr_accessor :silencer + attr_reader :local_levels self.silencer = true end + def after_initialize + @local_levels = Concurrent::Map.new(:initial_capacity => 2) + end + + def local_log_id + Thread.current.__id__ + end + + def level + local_levels[local_log_id] || super + end + # Silences the logger for the duration of the block. def silence(temporary_level = Logger::ERROR) if silencer begin - old_logger_level, self.level = level, temporary_level + old_local_level = local_levels[local_log_id] + local_levels[local_log_id] = temporary_level + yield self ensure - self.level = old_logger_level + if old_local_level + local_levels[local_log_id] = old_local_level + else + local_levels.delete(local_log_id) + end end else yield self end end -end +end
\ No newline at end of file diff --git a/activesupport/lib/active_support/multibyte/unicode.rb b/activesupport/lib/active_support/multibyte/unicode.rb index 586002b03b..72b20fff06 100644 --- a/activesupport/lib/active_support/multibyte/unicode.rb +++ b/activesupport/lib/active_support/multibyte/unicode.rb @@ -87,19 +87,44 @@ module ActiveSupport pos += 1 previous = codepoints[pos-1] current = codepoints[pos] - if ( - # CR X LF - ( previous == database.boundary[:cr] and current == database.boundary[:lf] ) or - # L X (L|V|LV|LVT) - ( database.boundary[:l] === previous and in_char_class?(current, [:l,:v,:lv,:lvt]) ) or - # (LV|V) X (V|T) - ( in_char_class?(previous, [:lv,:v]) and in_char_class?(current, [:v,:t]) ) or - # (LVT|T) X (T) - ( in_char_class?(previous, [:lvt,:t]) and database.boundary[:t] === current ) or - # X Extend - (database.boundary[:extend] === current) - ) - else + + should_break = + # GB3. CR X LF + if previous == database.boundary[:cr] and current == database.boundary[:lf] + false + # GB4. (Control|CR|LF) ÷ + elsif previous and 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 and in_char_class?(current, [:l,:v,:lv,:lvt]) + false + # GB7. (LV|V) X (V|T) + elsif in_char_class?(previous, [:lv,:v]) and in_char_class?(current, [:v,:t]) + false + # GB8. (LVT|T) X (T) + elsif in_char_class?(previous, [:lvt,:t]) and database.boundary[:t] === current + false + # GB8a. Regional_Indicator X Regional_Indicator + elsif database.boundary[:regional_indicator] === previous and database.boundary[:regional_indicator] === current + false + # GB9. X Extend + elsif database.boundary[:extend] === current + false + # GB9a. X SpacingMark + elsif database.boundary[:spacingmark] === current + false + # GB9b. Prepend X + elsif database.boundary[:prepend] === previous + false + # GB10. Any ÷ Any + else + true + end + + if should_break unpacked << codepoints[marker..pos-1] marker = pos end diff --git a/activesupport/lib/active_support/notifications/instrumenter.rb b/activesupport/lib/active_support/notifications/instrumenter.rb index 67f2ee1a7f..91f94cb2d7 100644 --- a/activesupport/lib/active_support/notifications/instrumenter.rb +++ b/activesupport/lib/active_support/notifications/instrumenter.rb @@ -21,6 +21,7 @@ module ActiveSupport yield payload rescue Exception => e payload[:exception] = [e.class.name, e.message] + payload[:exception_object] = e raise e ensure finish_with_state listeners_state, name, payload diff --git a/activesupport/lib/active_support/number_helper.rb b/activesupport/lib/active_support/number_helper.rb index 248521e677..64d9e71f37 100644 --- a/activesupport/lib/active_support/number_helper.rb +++ b/activesupport/lib/active_support/number_helper.rb @@ -47,6 +47,14 @@ module ActiveSupport # Formats a +number+ into a currency string (e.g., $13.65). You # can customize the format in the +options+ hash. # + # The currency unit and number formatting of the current locale will be used + # unless otherwise specified in the provided options. No currency conversion + # is performed. If the user is given a way to change their locale, they will + # also be able to change the relative value of the currency displayed with + # this helper. If your application will ever support multiple locales, you + # may want to specify a constant <tt>:locale</tt> option or consider + # using a library capable of currency conversion. + # # ==== Options # # * <tt>:locale</tt> - Sets the locale to be used for formatting @@ -235,6 +243,8 @@ module ActiveSupport # number_to_human_size(1234567) # => 1.18 MB # number_to_human_size(1234567890) # => 1.15 GB # number_to_human_size(1234567890123) # => 1.12 TB + # number_to_human_size(1234567890123456) # => 1.1 PB + # number_to_human_size(1234567890123456789) # => 1.07 EB # number_to_human_size(1234567, precision: 2) # => 1.2 MB # number_to_human_size(483989, precision: 2) # => 470 KB # number_to_human_size(1234567, precision: 2, separator: ',') # => 1,2 MB 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 7986eb50f0..57f40f33bf 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,3 +1,5 @@ +require 'active_support/core_ext/numeric/inquiry' + module ActiveSupport module NumberHelper class NumberToCurrencyConverter < NumberConverter # :nodoc: @@ -7,7 +9,7 @@ module ActiveSupport number = self.number.to_s.strip format = options[:format] - if is_negative?(number) + if number.to_f.negative? format = options[:negative_format] number = absolute_value(number) end @@ -18,10 +20,6 @@ module ActiveSupport private - def is_negative?(number) - number.to_f.phase != 0 - end - def absolute_value(number) number.respond_to?(:abs) ? number.abs : number.sub(/\A-/, '') end 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 a4a8690bcd..a83b368b7f 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,7 +1,7 @@ module ActiveSupport module NumberHelper class NumberToHumanSizeConverter < NumberConverter #:nodoc: - STORAGE_UNITS = [:byte, :kb, :mb, :gb, :tb] + STORAGE_UNITS = [:byte, :kb, :mb, :gb, :tb, :pb, :eb] self.namespace = :human self.validate_float = true diff --git a/activesupport/lib/active_support/security_utils.rb b/activesupport/lib/active_support/security_utils.rb index 64c4801179..9be8613ada 100644 --- a/activesupport/lib/active_support/security_utils.rb +++ b/activesupport/lib/active_support/security_utils.rb @@ -1,3 +1,5 @@ +require 'digest' + module ActiveSupport module SecurityUtils # Constant time string comparison. @@ -16,5 +18,10 @@ module ActiveSupport res == 0 end module_function :secure_compare + + def variable_size_secure_compare(a, b) # :nodoc: + secure_compare(::Digest::SHA256.hexdigest(a), ::Digest::SHA256.hexdigest(b)) + end + module_function :variable_size_secure_compare end end diff --git a/activesupport/lib/active_support/test_case.rb b/activesupport/lib/active_support/test_case.rb index ae6f00b861..d9a668c0ea 100644 --- a/activesupport/lib/active_support/test_case.rb +++ b/activesupport/lib/active_support/test_case.rb @@ -9,7 +9,6 @@ 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/composite_filter' require 'active_support/core_ext/kernel/reporting' module ActiveSupport @@ -39,15 +38,6 @@ module ActiveSupport def test_order ActiveSupport.test_order ||= :random end - - def run(reporter, options = {}) - if options[:patterns] && options[:patterns].any? { |p| p =~ /:\d+/ } - options[:filter] = \ - Testing::CompositeFilter.new(self, options[:filter], options[:patterns]) - end - - super - end end alias_method :method_name, :name diff --git a/activesupport/lib/active_support/testing/composite_filter.rb b/activesupport/lib/active_support/testing/composite_filter.rb deleted file mode 100644 index bde723e30b..0000000000 --- a/activesupport/lib/active_support/testing/composite_filter.rb +++ /dev/null @@ -1,54 +0,0 @@ -require 'method_source' - -module ActiveSupport - module Testing - class CompositeFilter # :nodoc: - def initialize(runnable, filter, patterns) - @runnable = runnable - @filters = [ derive_regexp(filter), *derive_line_filters(patterns) ].compact - end - - def ===(method) - @filters.any? { |filter| filter === method } - end - - private - def derive_regexp(filter) - filter =~ %r%/(.*)/% ? Regexp.new($1) : filter - end - - def derive_line_filters(patterns) - patterns.map do |file_and_line| - file, line = file_and_line.split(':') - Filter.new(@runnable, file, line) if file - end - end - - class Filter # :nodoc: - def initialize(runnable, file, line) - @runnable, @file = runnable, File.expand_path(file) - @line = line.to_i if line - end - - def ===(method) - return unless @runnable.method_defined?(method) - - if @line - test_file, test_range = definition_for(@runnable.instance_method(method)) - test_file == @file && test_range.include?(@line) - else - @runnable.instance_method(method).source_location.first == @file - end - end - - private - def definition_for(method) - file, start_line = method.source_location - end_line = method.source.count("\n") + start_line - 1 - - return file, start_line..end_line - end - end - end - end -end diff --git a/activesupport/test/broadcast_logger_test.rb b/activesupport/test/broadcast_logger_test.rb index e7d56c80c3..6d4e3b74f7 100644 --- a/activesupport/test/broadcast_logger_test.rb +++ b/activesupport/test/broadcast_logger_test.rb @@ -2,69 +2,56 @@ require 'abstract_unit' module ActiveSupport class BroadcastLoggerTest < TestCase - attr_reader :logger, :receiving_logger + attr_reader :logger, :log1, :log2 def setup - @logger = FakeLogger.new - @receiving_logger = FakeLogger.new - @logger.extend Logger.broadcast @receiving_logger + @log1 = FakeLogger.new + @log2 = FakeLogger.new + @log1.extend Logger.broadcast @log2 + @logger = @log1 end def test_debug logger.debug "foo" - assert_equal 'foo', logger.adds.first[2] - assert_equal 'foo', receiving_logger.adds.first[2] - end - - def test_debug_without_message_broadcasts - logger.broadcast_messages = false - logger.debug "foo" - assert_equal 'foo', logger.adds.first[2] - assert_equal [], receiving_logger.adds + assert_equal 'foo', log1.adds.first[2] + assert_equal 'foo', log2.adds.first[2] end def test_close logger.close - assert logger.closed, 'should be closed' - assert receiving_logger.closed, 'should be closed' + assert log1.closed, 'should be closed' + assert log2.closed, 'should be closed' end def test_chevrons logger << "foo" - assert_equal %w{ foo }, logger.chevrons - assert_equal %w{ foo }, receiving_logger.chevrons - end - - def test_chevrons_without_message_broadcasts - logger.broadcast_messages = false - logger << "foo" - assert_equal %w{ foo }, logger.chevrons - assert_equal [], receiving_logger.chevrons + assert_equal %w{ foo }, log1.chevrons + assert_equal %w{ foo }, log2.chevrons end def test_level assert_nil logger.level logger.level = 10 - assert_equal 10, logger.level - assert_equal 10, receiving_logger.level + assert_equal 10, log1.level + assert_equal 10, log2.level end def test_progname assert_nil logger.progname logger.progname = 10 - assert_equal 10, logger.progname - assert_equal 10, receiving_logger.progname + assert_equal 10, log1.progname + assert_equal 10, log2.progname end def test_formatter assert_nil logger.formatter logger.formatter = 10 - assert_equal 10, logger.formatter - assert_equal 10, receiving_logger.formatter + assert_equal 10, log1.formatter + assert_equal 10, log2.formatter end class FakeLogger attr_reader :adds, :closed, :chevrons - attr_accessor :level, :progname, :formatter, :broadcast_messages + attr_accessor :level, :progname, :formatter def initialize @adds = [] @@ -73,7 +60,6 @@ module ActiveSupport @level = nil @progname = nil @formatter = nil - @broadcast_messages = true end def debug msg, &block diff --git a/activesupport/test/caching_test.rb b/activesupport/test/caching_test.rb index 2701dc2fe9..4a299429f3 100644 --- a/activesupport/test/caching_test.rb +++ b/activesupport/test/caching_test.rb @@ -780,10 +780,12 @@ class FileStoreTest < ActiveSupport::TestCase include AutoloadingCacheBehavior def test_clear - filepath = File.join(cache_dir, ".gitkeep") - FileUtils.touch(filepath) + gitkeep = File.join(cache_dir, ".gitkeep") + keep = File.join(cache_dir, ".keep") + FileUtils.touch([gitkeep, keep]) @cache.clear - assert File.exist?(filepath) + assert File.exist?(gitkeep) + assert File.exist?(keep) end def test_clear_without_cache_dir diff --git a/activesupport/test/core_ext/hash_ext_test.rb b/activesupport/test/core_ext/hash_ext_test.rb index 2119352df0..be8583e704 100644 --- a/activesupport/test/core_ext/hash_ext_test.rb +++ b/activesupport/test/core_ext/hash_ext_test.rb @@ -702,6 +702,12 @@ class HashExtTest < ActiveSupport::TestCase assert_equal h.class, h.dup.class 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 + def test_assert_valid_keys assert_nothing_raised do { :failure => "stuff", :funny => "business" }.assert_valid_keys([ :failure, :funny ]) @@ -1587,9 +1593,9 @@ class HashToXmlTest < ActiveSupport::TestCase assert_equal 3, hash_wia[:new_key] end - def test_should_use_default_proc_if_no_key_is_supplied + def test_should_return_nil_if_no_key_is_supplied hash_wia = HashWithIndifferentAccess.new { 1 + 2 } - assert_equal 3, hash_wia.default + assert_equal nil, hash_wia.default end def test_should_use_default_value_for_unknown_key diff --git a/activesupport/test/core_ext/numeric_ext_test.rb b/activesupport/test/core_ext/numeric_ext_test.rb index 0ff8f0f89b..5654aeb4f8 100644 --- a/activesupport/test/core_ext/numeric_ext_test.rb +++ b/activesupport/test/core_ext/numeric_ext_test.rb @@ -143,6 +143,14 @@ class NumericExtFormattingTest < ActiveSupport::TestCase gigabytes(number) * 1024 end + def petabytes(number) + terabytes(number) * 1024 + end + + def exabytes(number) + petabytes(number) * 1024 + end + def test_to_s__phone assert_equal("555-1234", 5551234.to_s(:phone)) assert_equal("800-555-1212", 8005551212.to_s(:phone)) @@ -266,7 +274,9 @@ class NumericExtFormattingTest < ActiveSupport::TestCase assert_equal '1.18 MB', 1234567.to_s(:human_size) assert_equal '1.15 GB', 1234567890.to_s(:human_size) assert_equal '1.12 TB', 1234567890123.to_s(:human_size) - assert_equal '1030 TB', terabytes(1026).to_s(:human_size) + assert_equal '1.1 PB', 1234567890123456.to_s(:human_size) + assert_equal '1.07 EB', 1234567890123456789.to_s(:human_size) + assert_equal '1030 EB', exabytes(1026).to_s(:human_size) assert_equal '444 KB', kilobytes(444).to_s(:human_size) assert_equal '1020 MB', megabytes(1023).to_s(:human_size) assert_equal '3 TB', terabytes(3).to_s(:human_size) @@ -289,6 +299,8 @@ class NumericExtFormattingTest < ActiveSupport::TestCase assert_equal '1.23 MB', 1234567.to_s(:human_size, :prefix => :si) assert_equal '1.23 GB', 1234567890.to_s(:human_size, :prefix => :si) assert_equal '1.23 TB', 1234567890123.to_s(:human_size, :prefix => :si) + assert_equal '1.23 PB', 1234567890123456.to_s(:human_size, :prefix => :si) + assert_equal '1.23 EB', 1234567890123456789.to_s(:human_size, :prefix => :si) end end @@ -388,6 +400,32 @@ class NumericExtFormattingTest < ActiveSupport::TestCase assert_equal '1 Million', BigDecimal("1000010").to_s(:human) end + def test_to_formatted_s_is_deprecated + assert_deprecated do + 5551234.to_formatted_s(:phone) + end + end + + def test_to_s_with_invalid_formatter + assert_equal '123', 123.to_s(:invalid) + assert_equal '2.5', 2.5.to_s(:invalid) + assert_equal '100000000000000000000', (100**10).to_s(:invalid) + assert_equal '1000010.0', BigDecimal("1000010").to_s(:invalid) + end + + def test_default_to_s + assert_equal '123', 123.to_s + assert_equal '1111011', 123.to_s(2) + + assert_equal '2.5', 2.5.to_s + + assert_equal '100000000000000000000', (100**10).to_s + assert_equal '1010110101111000111010111100010110101100011000100000000000000000000', (100**10).to_s(2) + + assert_equal '1000010.0', BigDecimal("1000010").to_s + assert_equal '10000 10.0', BigDecimal("1000010").to_s('5F') + end + def test_in_milliseconds assert_equal 10_000, 10.seconds.in_milliseconds end diff --git a/activesupport/test/core_ext/range_ext_test.rb b/activesupport/test/core_ext/range_ext_test.rb index f096328cee..f28cebda3d 100644 --- a/activesupport/test/core_ext/range_ext_test.rb +++ b/activesupport/test/core_ext/range_ext_test.rb @@ -1,5 +1,6 @@ require 'abstract_unit' require 'active_support/time' +require 'active_support/core_ext/numeric' require 'active_support/core_ext/range' class RangeTest < ActiveSupport::TestCase @@ -13,6 +14,11 @@ class RangeTest < ActiveSupport::TestCase assert_equal "BETWEEN '2005-12-10 15:30:00' AND '2005-12-10 17:30:00'", date_range.to_s(:db) end + def test_to_s_with_numeric + number_range = (1..100) + assert_equal "BETWEEN '1' AND '100'", number_range.to_s(:db) + end + def test_date_range assert_instance_of Range, DateTime.new..DateTime.new assert_instance_of Range, DateTime::Infinity.new..DateTime::Infinity.new diff --git a/activesupport/test/logger_test.rb b/activesupport/test/logger_test.rb index d2801849ca..317e09b7f2 100644 --- a/activesupport/test/logger_test.rb +++ b/activesupport/test/logger_test.rb @@ -3,6 +3,7 @@ require 'multibyte_test_helpers' require 'stringio' require 'fileutils' require 'tempfile' +require 'concurrent/atomics' class LoggerTest < ActiveSupport::TestCase include MultibyteTestHelpers @@ -16,6 +17,14 @@ class LoggerTest < ActiveSupport::TestCase @logger = Logger.new(@output) end + def test_log_outputs_to + assert Logger.logger_outputs_to?(@logger, @output), "Expected logger_outputs_to? @output to return true but was false" + assert Logger.logger_outputs_to?(@logger, @output, STDOUT), "Expected logger_outputs_to? @output or STDOUT to return true but was false" + + assert_not Logger.logger_outputs_to?(@logger, STDOUT), "Expected logger_outputs_to? to STDOUT to return false, but was true" + assert_not Logger.logger_outputs_to?(@logger, STDOUT, STDERR), "Expected logger_outputs_to? to STDOUT or STDERR to return false, but was true" + end + def test_write_binary_data_to_existing_file t = Tempfile.new ['development', 'log'] t.binmode @@ -64,7 +73,7 @@ class LoggerTest < ActiveSupport::TestCase def test_should_not_log_debug_messages_when_log_level_is_info @logger.level = Logger::INFO @logger.add(Logger::DEBUG, @message) - assert ! @output.string.include?(@message) + assert_not @output.string.include?(@message) end def test_should_add_message_passed_as_block_when_using_add @@ -113,6 +122,7 @@ class LoggerTest < ActiveSupport::TestCase end def test_buffer_multibyte + @logger.level = Logger::INFO @logger.info(UNICODE_STRING) @logger.info(BYTE_STRING) assert @output.string.include?(UNICODE_STRING) @@ -120,14 +130,93 @@ class LoggerTest < ActiveSupport::TestCase byte_string.force_encoding("ASCII-8BIT") assert byte_string.include?(BYTE_STRING) end - + def test_silencing_everything_but_errors @logger.silence do @logger.debug "NOT THERE" @logger.error "THIS IS HERE" end - - assert !@output.string.include?("NOT THERE") + + assert_not @output.string.include?("NOT THERE") assert @output.string.include?("THIS IS HERE") end + + def test_logger_level_per_object_thread_safety + logger1 = Logger.new(StringIO.new) + logger2 = Logger.new(StringIO.new) + + level = Logger::DEBUG + assert_equal level, logger1.level, "Expected level #{level_name(level)}, got #{level_name(logger1.level)}" + assert_equal level, logger2.level, "Expected level #{level_name(level)}, got #{level_name(logger2.level)}" + + logger1.level = Logger::ERROR + assert_equal level, logger2.level, "Expected level #{level_name(level)}, got #{level_name(logger2.level)}" + end + + def test_logger_level_main_thread_safety + @logger.level = Logger::INFO + assert_level(Logger::INFO) + + latch = Concurrent::CountDownLatch.new + latch2 = Concurrent::CountDownLatch.new + + t = Thread.new do + latch.wait + assert_level(Logger::INFO) + latch2.count_down + end + + @logger.silence(Logger::ERROR) do + assert_level(Logger::ERROR) + latch.count_down + latch2.wait + end + + t.join + end + + def test_logger_level_local_thread_safety + @logger.level = Logger::INFO + assert_level(Logger::INFO) + + thread_1_latch = Concurrent::CountDownLatch.new + thread_2_latch = Concurrent::CountDownLatch.new + + threads = (1..2).collect do |thread_number| + Thread.new do + # force thread 2 to wait until thread 1 is already in @logger.silence + thread_2_latch.wait if thread_number == 2 + + @logger.silence(Logger::ERROR) do + assert_level(Logger::ERROR) + @logger.silence(Logger::DEBUG) do + # allow thread 2 to finish but hold thread 1 + if thread_number == 1 + thread_2_latch.count_down + thread_1_latch.wait + end + assert_level(Logger::DEBUG) + end + end + + # allow thread 1 to finish + assert_level(Logger::INFO) + thread_1_latch.count_down if thread_number == 2 + end + end + + threads.each(&:join) + assert_level(Logger::INFO) + end + + private + def level_name(level) + ::Logger::Severity.constants.find do |severity| + Logger.const_get(severity) == level + end.to_s + end + + def assert_level(level) + assert_equal level, @logger.level, "Expected level #{level_name(level)}, got #{level_name(@logger.level)}" + end end diff --git a/activesupport/test/multibyte_chars_test.rb b/activesupport/test/multibyte_chars_test.rb index 8d4d9d736c..c1e0b19248 100644 --- a/activesupport/test/multibyte_chars_test.rb +++ b/activesupport/test/multibyte_chars_test.rb @@ -612,28 +612,54 @@ class MultibyteCharsExtrasTest < ActiveSupport::TestCase ['abc', 3], ['こにちわ', 4], [[0x0924, 0x094D, 0x0930].pack('U*'), 2], + # GB3 [%w(cr lf), 1], + # GB4 + [%w(cr n), 2], + [%w(lf n), 2], + [%w(control n), 2], + [%w(cr extend), 2], + [%w(lf extend), 2], + [%w(control extend), 2], + # GB 5 + [%w(n cr), 2], + [%w(n lf), 2], + [%w(n control), 2], + [%w(extend cr), 2], + [%w(extend lf), 2], + [%w(extend control), 2], + # GB 6 [%w(l l), 1], [%w(l v), 1], [%w(l lv), 1], [%w(l lvt), 1], + # GB7 [%w(lv v), 1], [%w(lv t), 1], [%w(v v), 1], [%w(v t), 1], + # GB8 [%w(lvt t), 1], [%w(t t), 1], + # GB8a + [%w(r r), 1], + # GB9 [%w(n extend), 1], + # GB9a + [%w(n spacingmark), 1], + # GB10 [%w(n n), 2], + # Other [%w(n cr lf n), 3], - [%w(n l v t), 2] + [%w(n l v t), 2], + [%w(cr extend n), 3], ].each do |input, expected_length| if input.kind_of?(Array) str = string_from_classes(input) else str = input end - assert_equal expected_length, chars(str).grapheme_length + assert_equal expected_length, chars(str).grapheme_length, input.inspect end end @@ -698,7 +724,7 @@ class MultibyteCharsExtrasTest < ActiveSupport::TestCase # Characters from the character classes as described in UAX #29 character_from_class = { :l => 0x1100, :v => 0x1160, :t => 0x11A8, :lv => 0xAC00, :lvt => 0xAC01, :cr => 0x000D, :lf => 0x000A, - :extend => 0x094D, :n => 0x64 + :extend => 0x094D, :n => 0x64, :spacingmark => 0x0903, :r => 0x1F1E6, :control => 0x0001 } classes.collect do |k| character_from_class[k.intern] diff --git a/activesupport/test/multibyte_conformance_test.rb b/activesupport/test/multibyte_conformance_test.rb index d493a48fe4..5df8f32e46 100644 --- a/activesupport/test/multibyte_conformance_test.rb +++ b/activesupport/test/multibyte_conformance_test.rb @@ -5,25 +5,25 @@ require 'fileutils' require 'open-uri' require 'tmpdir' -class Downloader - def self.download(from, to) - unless File.exist?(to) - unless File.exist?(File.dirname(to)) - system "mkdir -p #{File.dirname(to)}" - end - open(from) do |source| - File.open(to, 'w') do |target| - source.each_line do |l| - target.write l +class MultibyteConformanceTest < ActiveSupport::TestCase + class Downloader + def self.download(from, to) + unless File.exist?(to) + unless File.exist?(File.dirname(to)) + system "mkdir -p #{File.dirname(to)}" + end + open(from) do |source| + File.open(to, 'w') do |target| + source.each_line do |l| + target.write l + end end end end + true end - true end -end -class MultibyteConformanceTest < ActiveSupport::TestCase include MultibyteTestHelpers UNIDATA_URL = "http://www.unicode.org/Public/#{ActiveSupport::Multibyte::Unicode::UNICODE_VERSION}/ucd" diff --git a/activesupport/test/multibyte_grapheme_break_conformance_test.rb b/activesupport/test/multibyte_grapheme_break_conformance_test.rb new file mode 100644 index 0000000000..229f24990e --- /dev/null +++ b/activesupport/test/multibyte_grapheme_break_conformance_test.rb @@ -0,0 +1,76 @@ +# encoding: utf-8 + +require 'abstract_unit' + +require 'fileutils' +require 'open-uri' +require 'tmpdir' + +class MultibyteGraphemeBreakConformanceTest < ActiveSupport::TestCase + class Downloader + def self.download(from, to) + unless File.exist?(to) + $stderr.puts "Downloading #{from} to #{to}" + unless File.exist?(File.dirname(to)) + system "mkdir -p #{File.dirname(to)}" + end + open(from) do |source| + File.open(to, 'w') do |target| + source.each_line do |l| + target.write l + end + end + end + end + end + end + + TEST_DATA_URL = "http://www.unicode.org/Public/#{ActiveSupport::Multibyte::Unicode::UNICODE_VERSION}/ucd/auxiliary" + TEST_DATA_FILE = '/GraphemeBreakTest.txt' + CACHE_DIR = File.join(Dir.tmpdir, 'cache') + + def setup + FileUtils.mkdir_p(CACHE_DIR) + Downloader.download(TEST_DATA_URL + TEST_DATA_FILE, CACHE_DIR + TEST_DATA_FILE) + 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 + end + end + + protected + def each_line_of_break_tests(&block) + lines = 0 + max_test_lines = 0 # Don't limit below 21, because that's the header of the testfile + File.open(File.join(CACHE_DIR, TEST_DATA_FILE), 'r') do | f | + until f.eof? || (max_test_lines > 21 and lines > max_test_lines) + lines += 1 + line = f.gets.chomp! + next if (line.empty? || line =~ /^\#/) + + cols, comment = line.split("#") + # Cluster breaks are represented by ÷ + clusters = cols.split("÷").map{|e| e.strip}.reject{|e| e.empty? } + clusters = clusters.map do |cluster| + # Codepoints within each cluster are separated by × + codepoints = cluster.split("×").map{|e| e.strip}.reject{|e| e.empty? } + # codepoints are in hex in the test suite, pack wants them as integers + codepoints.map{|codepoint| codepoint.to_i(16)} + end + + # The tests contain a solitary U+D800 <Non Private Use High + # Surrogate, First> character, which Ruby does not allow to stand + # alone in a UTF-8 string. So we'll just skip it. + next if clusters.flatten.include?(0xd800) + + clusters << comment.strip + + yield(*clusters) + end + end + end +end diff --git a/activesupport/test/multibyte_normalization_conformance_test.rb b/activesupport/test/multibyte_normalization_conformance_test.rb new file mode 100644 index 0000000000..8bc91ef708 --- /dev/null +++ b/activesupport/test/multibyte_normalization_conformance_test.rb @@ -0,0 +1,129 @@ +# encoding: utf-8 + +require 'abstract_unit' +require 'multibyte_test_helpers' + +require 'fileutils' +require 'open-uri' +require 'tmpdir' + +class MultibyteNormalizationConformanceTest < ActiveSupport::TestCase + class Downloader + def self.download(from, to) + unless File.exist?(to) + $stderr.puts "Downloading #{from} to #{to}" + unless File.exist?(File.dirname(to)) + system "mkdir -p #{File.dirname(to)}" + end + open(from) do |source| + File.open(to, 'w') do |target| + source.each_line do |l| + target.write l + end + end + end + end + end + end + + include MultibyteTestHelpers + + UNIDATA_URL = "http://www.unicode.org/Public/#{ActiveSupport::Multibyte::Unicode::UNICODE_VERSION}/ucd" + UNIDATA_FILE = '/NormalizationTest.txt' + CACHE_DIR = File.join(Dir.tmpdir, 'cache') + + def setup + FileUtils.mkdir_p(CACHE_DIR) + Downloader.download(UNIDATA_URL + UNIDATA_FILE, CACHE_DIR + UNIDATA_FILE) + @proxy = ActiveSupport::Multibyte::Chars + end + + def test_normalizations_C + 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}" + 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}" + 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}" + 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}" + end + end + + protected + def each_line_of_norm_tests(&block) + lines = 0 + max_test_lines = 0 # Don't limit below 38, because that's the header of the testfile + File.open(File.join(CACHE_DIR, UNIDATA_FILE), 'r') do | f | + until f.eof? || (max_test_lines > 38 and lines > max_test_lines) + lines += 1 + line = f.gets.chomp! + next if (line.empty? || line =~ /^\#/) + + cols, comment = line.split("#") + cols = cols.split(";").map{|e| e.strip}.reject{|e| e.empty? } + next unless cols.length == 5 + + # codepoints are in hex in the test suite, pack wants them as integers + cols.map!{|c| c.split.map{|codepoint| codepoint.to_i(16)}.pack("U*") } + cols << comment + + yield(*cols) + end + end + end + + def inspect_codepoints(str) + str.to_s.unpack("U*").map{|cp| cp.to_s(16) }.join(' ') + end +end diff --git a/activesupport/test/notifications_test.rb b/activesupport/test/notifications_test.rb index d9cc392ac9..1cb17e6197 100644 --- a/activesupport/test/notifications_test.rb +++ b/activesupport/test/notifications_test.rb @@ -232,7 +232,7 @@ module Notifications assert_equal 1, @events.size assert_equal Hash[:payload => "notifications", - :exception => ["RuntimeError", "FAIL"]], @events.last.payload + :exception => ["RuntimeError", "FAIL"], :exception_object => e], @events.last.payload end def test_event_is_pushed_even_without_block diff --git a/activesupport/test/number_helper_test.rb b/activesupport/test/number_helper_test.rb index 7f62d7c0b3..6696111476 100644 --- a/activesupport/test/number_helper_test.rb +++ b/activesupport/test/number_helper_test.rb @@ -34,6 +34,14 @@ module ActiveSupport gigabytes(number) * 1024 end + def petabytes(number) + terabytes(number) * 1024 + end + + def exabytes(number) + petabytes(number) * 1024 + end + def test_number_to_phone [@instance_with_helpers, TestClassWithClassNumberHelpers, ActiveSupport::NumberHelper].each do |number_helper| assert_equal("555-1234", number_helper.number_to_phone(5551234)) @@ -66,7 +74,6 @@ module ActiveSupport assert_equal("1,234,567,890.50 Kč", number_helper.number_to_currency("1234567890.50", {:unit => "Kč", :format => "%n %u"})) assert_equal("1,234,567,890.50 - Kč", number_helper.number_to_currency("-1234567890.50", {:unit => "Kč", :format => "%n %u", :negative_format => "%n - %u"})) assert_equal("0.00", number_helper.number_to_currency(+0.0, {:unit => "", :negative_format => "(%n)"})) - assert_equal("(0.00)", number_helper.number_to_currency(-0.0, {:unit => "", :negative_format => "(%n)"})) end end @@ -219,7 +226,9 @@ module ActiveSupport assert_equal '1.18 MB', number_helper.number_to_human_size(1234567) assert_equal '1.15 GB', number_helper.number_to_human_size(1234567890) assert_equal '1.12 TB', number_helper.number_to_human_size(1234567890123) - assert_equal '1030 TB', number_helper.number_to_human_size(terabytes(1026)) + assert_equal '1.1 PB', number_helper.number_to_human_size(1234567890123456) + assert_equal '1.07 EB', number_helper.number_to_human_size(1234567890123456789) + assert_equal '1030 EB', number_helper.number_to_human_size(exabytes(1026)) assert_equal '444 KB', number_helper.number_to_human_size(kilobytes(444)) assert_equal '1020 MB', number_helper.number_to_human_size(megabytes(1023)) assert_equal '3 TB', number_helper.number_to_human_size(terabytes(3)) @@ -245,6 +254,8 @@ module ActiveSupport assert_equal '1.23 MB', number_helper.number_to_human_size(1234567, :prefix => :si) assert_equal '1.23 GB', number_helper.number_to_human_size(1234567890, :prefix => :si) assert_equal '1.23 TB', number_helper.number_to_human_size(1234567890123, :prefix => :si) + assert_equal '1.23 PB', number_helper.number_to_human_size(1234567890123456, :prefix => :si) + assert_equal '1.23 EB', number_helper.number_to_human_size(1234567890123456789, :prefix => :si) end end end diff --git a/activesupport/test/share_lock_test.rb b/activesupport/test/share_lock_test.rb index 465a657308..acefa185a8 100644 --- a/activesupport/test/share_lock_test.rb +++ b/activesupport/test/share_lock_test.rb @@ -114,14 +114,17 @@ class ShareLockTest < ActiveSupport::TestCase [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 end @@ -183,11 +186,14 @@ class ShareLockTest < ActiveSupport::TestCase load_params = [:load, [:load]] unload_params = [:unload, [:unload, :load]] + all_sharing = Concurrent::CyclicBarrier.new(4) + [load_params, load_params, unload_params, unload_params].permutation do |thread_params| with_thread_waiting_in_lock_section(:sharing) do |sharing_thread_release_latch| threads = thread_params.map do |purpose, compatible| Thread.new do @lock.sharing do + all_sharing.wait @lock.exclusive(purpose: purpose, compatible: compatible) do scratch_pad_mutex.synchronize { scratch_pad << purpose } end @@ -209,6 +215,245 @@ class ShareLockTest < ActiveSupport::TestCase end end + def test_new_share_attempts_block_on_waiting_exclusive + with_thread_waiting_in_lock_section(:sharing) do |sharing_thread_release_latch| + release_exclusive = Concurrent::CountDownLatch.new + + waiting_exclusive = Thread.new do + @lock.sharing do + @lock.exclusive do + release_exclusive.wait + end + end + end + assert_threads_stuck waiting_exclusive + + late_share_attempt = Thread.new do + @lock.sharing {} + end + assert_threads_stuck late_share_attempt + + sharing_thread_release_latch.count_down + assert_threads_stuck late_share_attempt + + release_exclusive.count_down + assert_threads_not_stuck late_share_attempt + end + end + + def test_share_remains_reentrant_ignoring_a_waiting_exclusive + with_thread_waiting_in_lock_section(:sharing) do |sharing_thread_release_latch| + ready = Concurrent::CyclicBarrier.new(2) + attempt_reentrancy = Concurrent::CountDownLatch.new + + sharer = Thread.new do + @lock.sharing do + ready.wait + attempt_reentrancy.wait + @lock.sharing {} + end + end + + exclusive = Thread.new do + @lock.sharing do + ready.wait + @lock.exclusive {} + end + end + + assert_threads_stuck exclusive + + attempt_reentrancy.count_down + + assert_threads_not_stuck sharer + assert_threads_stuck exclusive + end + end + + def test_compatible_exclusives_cooperate_to_both_proceed + ready = Concurrent::CyclicBarrier.new(2) + done = Concurrent::CyclicBarrier.new(2) + + threads = 2.times.map do + Thread.new do + @lock.sharing do + ready.wait + @lock.exclusive(purpose: :x, compatible: [:x], after_compatible: [:x]) {} + done.wait + end + end + end + + assert_threads_not_stuck threads + end + + def test_manual_yield + ready = Concurrent::CyclicBarrier.new(2) + done = Concurrent::CyclicBarrier.new(2) + + threads = [ + Thread.new do + @lock.sharing do + ready.wait + @lock.exclusive(purpose: :x) {} + done.wait + end + end, + + Thread.new do + @lock.sharing do + ready.wait + @lock.yield_shares(compatible: [:x]) do + done.wait + end + end + end, + ] + + assert_threads_not_stuck threads + end + + def test_manual_incompatible_yield + ready = Concurrent::CyclicBarrier.new(2) + done = Concurrent::CyclicBarrier.new(2) + + threads = [ + Thread.new do + @lock.sharing do + ready.wait + @lock.exclusive(purpose: :x) {} + done.wait + end + end, + + Thread.new do + @lock.sharing do + ready.wait + @lock.yield_shares(compatible: [:y]) do + done.wait + end + end + end, + ] + + assert_threads_stuck threads + ensure + threads.each(&:kill) if threads + end + + def test_manual_recursive_yield + ready = Concurrent::CyclicBarrier.new(2) + done = Concurrent::CyclicBarrier.new(2) + do_nesting = Concurrent::CountDownLatch.new + + threads = [ + Thread.new do + @lock.sharing do + ready.wait + @lock.exclusive(purpose: :x) {} + done.wait + end + end, + + Thread.new do + @lock.sharing do + @lock.yield_shares(compatible: [:x]) do + @lock.sharing do + ready.wait + do_nesting.wait + @lock.yield_shares(compatible: [:x, :y]) do + done.wait + end + end + end + end + end + ] + + assert_threads_stuck threads + do_nesting.count_down + + assert_threads_not_stuck threads + end + + def test_manual_recursive_yield_cannot_expand_outer_compatible + ready = Concurrent::CyclicBarrier.new(2) + do_compatible_nesting = Concurrent::CountDownLatch.new + in_compatible_nesting = Concurrent::CountDownLatch.new + + incompatible_thread = Thread.new do + @lock.sharing do + ready.wait + @lock.exclusive(purpose: :x) {} + end + end + + yield_shares_thread = Thread.new do + @lock.sharing do + ready.wait + @lock.yield_shares(compatible: [:y]) do + do_compatible_nesting.wait + @lock.sharing do + @lock.yield_shares(compatible: [:x, :y]) do + in_compatible_nesting.wait + end + end + end + end + end + + assert_threads_stuck incompatible_thread + do_compatible_nesting.count_down + assert_threads_stuck incompatible_thread + in_compatible_nesting.count_down + assert_threads_not_stuck [yield_shares_thread, incompatible_thread] + end + + def test_manual_recursive_yield_restores_previous_compatible + ready = Concurrent::CyclicBarrier.new(2) + do_nesting = Concurrent::CountDownLatch.new + after_nesting = Concurrent::CountDownLatch.new + + incompatible_thread = Thread.new do + ready.wait + @lock.exclusive(purpose: :z) {} + end + + recursive_yield_shares_thread = Thread.new do + @lock.sharing do + ready.wait + @lock.yield_shares(compatible: [:y]) do + do_nesting.wait + @lock.sharing do + @lock.yield_shares(compatible: [:x, :y]) {} + end + after_nesting.wait + end + end + end + + assert_threads_stuck incompatible_thread + do_nesting.count_down + assert_threads_stuck incompatible_thread + + compatible_thread = Thread.new do + @lock.exclusive(purpose: :y) {} + end + assert_threads_not_stuck compatible_thread + + post_nesting_incompatible_thread = Thread.new do + @lock.exclusive(purpose: :x) {} + end + assert_threads_stuck post_nesting_incompatible_thread + + after_nesting.count_down + assert_threads_not_stuck recursive_yield_shares_thread + # post_nesting_incompatible_thread can now proceed + assert_threads_not_stuck post_nesting_incompatible_thread + # assert_threads_not_stuck can now proceed + assert_threads_not_stuck incompatible_thread + end + def test_in_shared_section_incompatible_non_upgrading_threads_cannot_preempt_upgrading_threads scratch_pad = [] scratch_pad_mutex = Mutex.new |