diff options
Diffstat (limited to 'activesupport')
211 files changed, 4143 insertions, 3471 deletions
diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md index f5f2aa85ef..cae4ee7fde 100644 --- a/activesupport/CHANGELOG.md +++ b/activesupport/CHANGELOG.md @@ -1,320 +1,243 @@ -## Rails 4.0.0 (unreleased) ## +* Fix `slice!` deleting the default value of the hash. -* Deprecate `ActiveSupport::BasicObject` in favor of `ActiveSupport::ProxyObject`. - This class is used for proxy classes. It avoids confusion with Ruby's BasicObject - class. + *Antonio Santos* - *Francesco Rodriguez* +* `require_dependency` accepts objects that respond to `to_path`, in + particular `Pathname` instances. -* Patched Marshal#load to work with constant autoloading. - Fixes autoloading with cache stores that relay on Marshal(MemCacheStore and FileStore). [fixes #8167] + *Benjamin Fleischer* - *Uriel Katz* +* Disable the ability to iterate over Range of AS::TimeWithZone + due to significant performance issues. -* Make `Time.zone.parse` to work with JavaScript format date strings. *Andrew White* - -* Add `DateTime#seconds_until_end_of_day` and `Time#seconds_until_end_of_day` - as a complement for `seconds_from_midnight`; useful when setting expiration - times for caches, e.g.: - - <% cache('dashboard', expires_in: Date.current.seconds_until_end_of_day) do %> - ... - - *Olek Janiszewski* - -* No longer proxy ActiveSupport::Multibyte#class. *Steve Klabnik* + *Bogdan Gusiev* -* Deprecate `ActiveSupport::TestCase#pending` method, use `skip` from MiniTest instead. *Carlos Antonio da Silva* +* Allow attaching event subscribers to ActiveSupport::Notifications namespaces + before they're defined. Essentially, this means instead of this: -* `XmlMini.with_backend` now may be safely used with threads: + class JokeSubscriber < ActiveSupport::Subscriber + def sql(event) + puts "A rabbi and a priest walk into a bar..." + end - Thread.new do - XmlMini.with_backend("REXML") { rexml_power } + # This call needs to happen *after* defining the methods. + attach_to "active_record" end - Thread.new do - XmlMini.with_backend("LibXML") { libxml_power } - end - - Each thread will use it's own backend. - *Nikita Afanasenko* + You can do this: -* Dependencies no longer trigger Kernel#autoload in remove_constant [fixes #8213]. *Xavier Noria* + class JokeSubscriber < ActiveSupport::Subscriber + # This is much easier to read! + attach_to "active_record" -* Simplify mocha integration and remove monkey-patches, bumping mocha to 0.13.0. *James Mead* - -* `#as_json` isolates options when encoding a hash. - Fix #8182 + def sql(event) + puts "A rabbi and a priest walk into a bar..." + end + end - *Yves Senn* + This should make it easier to read and understand these subscribers. -* Deprecate Hash#diff in favor of MiniTest's #diff. *Steve Klabnik* + *Daniel Schierbeck* -* Kernel#capture can catch output from subprocesses *Dmitry Vorotilin* +* Add `Date#middle_of_day`, `DateTime#middle_of_day` and `Time#middle_of_day` methods. -* `to_xml` conversions now use builder's `tag!` method instead of explicit invocation of `method_missing`. + Also added `midday`, `noon`, `at_midday`, `at_noon` and `at_middle_of_day` as aliases. - *Nikita Afanasenko* + *Anatoli Makarevich* -* Fixed timezone mapping of the Solomon Islands. *Steve Klabnik* +* Fix ActiveSupport::Cache::FileStore#cleanup to no longer rely on missing each_key method. -* Make callstack attribute optional in - ActiveSupport::Deprecation::Reporting methods `warn` and `deprecation_warning` + *Murray Steele* - *Alexey Gaziev* +* Ensure that autoloaded constants in all-caps nestings are marked as + autoloaded. -* Implement HashWithIndifferentAccess#replace so key? works correctly. *David Graham* + *Simon Coffey* -* Handle the possible Permission Denied errors atomic.rb might trigger due to its chown and chmod calls. *Daniele Sluijters* +* Add String#remove(pattern) as a short-hand for the common pattern of String#gsub(pattern, '') -* Hash#extract! returns only those keys that present in the receiver. + *DHH* - {:a => 1, :b => 2}.extract!(:a, :x) # => {:a => 1} +* Adds a new deprecation behaviour that raises an exception. Throwing this + line into +config/environments/development.rb+ - *Mikhail Dieterle* + ActiveSupport::Deprecation.behavior = :raise -* Hash#extract! returns the same subclass, that the receiver is. I.e. - HashWithIndifferentAccess#extract! returns HashWithIndifferentAccess instance. + will cause the application to raise an +ActiveSupport::DeprecationException+ + on deprecations. - *Mikhail Dieterle* + Use this for aggressive deprecation cleanups. -* Optimize ActiveSupport::Cache::Entry to reduce memory and processing overhead. *Brian Durand* + *Xavier Noria* -* Tests tag the Rails log with the current test class and test case: +* Remove 'cow' => 'kine' irregular inflection from default inflections. - [SessionsControllerTest] [test_0002_sign in] Processing by SessionsController#create as HTML - [SessionsControllerTest] [test_0002_sign in] ... + *Andrew White* - *Jeremy Kemper* +* Add `DateTime#to_s(:iso8601)` and `Date#to_s(:iso8601)` for consistency. -* Add `logger.push_tags` and `.pop_tags` to complement logger.tagged: + *Andrew White* - class Job - def before - Rails.logger.push_tags :jobs, self.class.name - end +* Add `Time#to_s(:iso8601)` for easy conversion of times to the iso8601 format for easy Javascript date parsing. - def after - Rails.logger.pop_tags 2 - end - end - - *Jeremy Kemper* + *DHH* -* Allow delegation to the class using the `:class` keyword, replacing - `self.class` usage: +* Improve `ActiveSupport::Cache::MemoryStore` cache size calculation. + The memory used by a key/entry pair is calculated via `#cached_size`: - class User - def self.hello - "world" - end - - delegate :hello, to: :class + def cached_size(key, entry) + key.to_s.bytesize + entry.size + PER_ENTRY_OVERHEAD end - *Marc-Andre Lafortune* - -* `Date.beginning_of_week` thread local and `beginning_of_week` application - config option added (default is Monday). - - *Innokenty Mikhailov* + The value of `PER_ENTRY_OVERHEAD` is 240 bytes based on an [empirical + estimation](https://gist.github.com/ssimeonov/6047200) for 64-bit MRI on + 1.9.3 and 2.0. GH#11512 -* An optional block can be passed to `config_accessor` to set its default value + *Simeon Simeonov* - class User - include ActiveSupport::Configurable - config_accessor :hair_colors do - [:brown, :black, :blonde, :red] - end - end +* Only raise `Module::DelegationError` if it's the source of the exception. - User.hair_colors # => [:brown, :black, :blonde, :red] + Fixes #10559 - *Larry Lv* + *Andrew White* -* ActiveSupport::Benchmarkable#silence has been deprecated due to its lack of - thread safety. It will be removed without replacement in Rails 4.1. +* Make `Time.at_with_coercion` retain the second fraction and return local time. - *Steve Klabnik* + Fixes #11350 -* An optional block can be passed to `Hash#deep_merge`. The block will be invoked - for each duplicated key and used to resolve the conflict. + *Neer Friedman*, *Andrew White* - *Pranas Kiziela* +* Make `HashWithIndifferentAccess#select` always return the hash, even when + `Hash#select!` returns `nil`, to allow further chaining. -* ActiveSupport::Deprecation is now a class. It is possible to create an instance - of deprecator. Backwards compatibility has been preserved. + *Marc Schütz* - You can choose which instance of the deprecator will be used. +* Remove deprecated `String#encoding_aware?` core extensions (`core_ext/string/encoding`). - deprecate :method_name, :deprecator => deprecator_instance + *Arun Agrawal* - You can use ActiveSupport::Deprecation in your gem. +* Remove deprecated `Module#local_constant_names` in favor of `Module#local_constants`. - require 'active_support/deprecation' - require 'active_support/core_ext/module/deprecation' + *Arun Agrawal* - class MyGem - def self.deprecator - ActiveSupport::Deprecation.new('2.0', 'MyGem') - end +* Remove deprecated `DateTime.local_offset` in favor of `DateTime.civil_from_fromat`. - def old_method - end + *Arun Agrawal* - def new_method - end - - deprecate :old_method => :new_method, :deprecator => deprecator - end +* Remove deprecated `Logger` core extensions (`core_ext/logger.rb`). - MyGem.new.old_method - # => DEPRECATION WARNING: old_method is deprecated and will be removed from MyGem 2.0 (use new_method instead). (called from <main> at file.rb:18) - - *Piotr Niełacny & Robert Pankowecki* - -* `ERB::Util.html_escape` encodes single quote as `#39`. Decimal form has better support in old browsers. *Kalys Osmonov* - -* `ActiveSupport::Callbacks`: deprecate monkey patch of object callbacks. - Using the #filter method like this: - - before_filter MyFilter.new - - class MyFilter - def filter(controller) - end - end - - Is now deprecated with recommendation to use the corresponding filter type - (`#before`, `#after` or `#around`): - - before_filter MyFilter.new - - class MyFilter - def before(controller) - end - end - - *Bogdan Gusiev* - -* An optional block can be passed to `HashWithIndifferentAccess#update` and `#merge`. - The block will be invoked for each duplicated key, and used to resolve the conflict, - thus replicating the behaviour of the corresponding methods on the `Hash` class. - - *Leo Cassarani* - -* Remove `j` alias for `ERB::Util#json_escape`. - The `j` alias is already used for `ActionView::Helpers::JavaScriptHelper#escape_javascript` - and both modules are included in the view context that would confuse the developers. + *Carlos Antonio da Silva* - *Akira Matsuda* +* Remove deprecated `Time#time_with_datetime_fallback`, `Time#utc_time` + and `Time#local_time` in favor of `Time#utc` and `Time#local`. -* Replace deprecated `memcache-client` gem with `dalli` in ActiveSupport::Cache::MemCacheStore + *Vipul A M* - *Guillermo Iguaran* +* Remove deprecated `Hash#diff` with no replacement. -* Add default values to all `ActiveSupport::NumberHelper` methods, to avoid - errors with empty locales or missing values. + If you're using it to compare hashes for the purpose of testing, please use + MiniTest's `assert_equal` instead. *Carlos Antonio da Silva* -* `ActiveSupport::JSON::Variable` is deprecated. Define your own `#as_json` and - `#encode_json` methods for custom JSON string literals. +* Remove deprecated `Date#to_time_in_current_zone` in favor of `Date#in_time_zone`. - *Erich Menge* + *Vipul A M* -* Add String#indent. *fxn & Ace Suares* +* Remove deprecated `Proc#bind` with no replacement. -* Inflections can now be defined per locale. `singularize` and `pluralize` - accept locale as an extra argument. - - *David Celis* + *Carlos Antonio da Silva* -* `Object#try` will now return nil instead of raise a NoMethodError if the - receiving object does not implement the method, but you can still get the - old behavior by using the new `Object#try!`. +* Remove deprecated `Array#uniq_by` and `Array#uniq_by!`, use native + `Array#uniq` and `Array#uniq!` instead. - *DHH* + *Carlos Antonio da Silva* -* `ERB::Util.html_escape` now escapes single quotes. *Santiago Pastorino* +* Remove deprecated `ActiveSupport::BasicObject`, use `ActiveSupport::ProxyObject` instead. -* `Time#change` now works with time values with offsets other than UTC or the local time zone. *Andrew White* + *Carlos Antonio da Silva* -* `ActiveSupport::Callbacks`: deprecate usage of filter object with `#before` and `#after` methods as `around` callback. *Bogdan Gusiev* +* Remove deprecated `BufferedLogger`. -* Add `Time#prev_quarter` and `Time#next_quarter` short-hands for `months_ago(3)` and `months_since(3)`. *SungHee Kang* + *Yves Senn* -* Remove obsolete and unused `require_association` method from dependencies. *fxn* +* Remove deprecated `assert_present` and `assert_blank` methods. -* Add `:instance_accessor` option for `config_accessor`. + *Yves Senn* - class User - include ActiveSupport::Configurable - config_accessor :allowed_access, instance_accessor: false - end +* Fix return value from `BacktraceCleaner#noise` when the cleaner is configured + with multiple silencers. - User.new.allowed_access = true # => NoMethodError - User.new.allowed_access # => NoMethodError + Fixes #11030 - *Francesco Rodriguez* + *Mark J. Titorenko* -* ActionView::Helpers::NumberHelper methods have been moved to ActiveSupport::NumberHelper and are now available via - Numeric#to_s. Numeric#to_s now accepts the formatting options :phone, :currency, :percentage, :delimited, - :rounded, :human, and :human_size. *Andrew Mutz* +* `HashWithIndifferentAccess#select` now returns a `HashWithIndifferentAccess` + instance instead of a `Hash` instance. -* Add `Hash#transform_keys`, `Hash#transform_keys!`, `Hash#deep_transform_keys`, and `Hash#deep_transform_keys!`. *Mark McSpadden* + Fixes #10723 -* Changed xml type `datetime` to `dateTime` (with upper case letter `T`). *Angelo Capilleri* + *Albert Llop* -* Add `:instance_accessor` option for `class_attribute`. *Alexey Vakhov* +* Add `DateTime#usec` and `DateTime#nsec` so that `ActiveSupport::TimeWithZone` keeps + sub-second resolution when wrapping a `DateTime` value. -* `constantize` now looks in the ancestor chain. *Marc-Andre Lafortune & Andrew White* + Fixes #10855 -* Adds `Hash#deep_stringify_keys` and `Hash#deep_stringify_keys!` to convert all keys from a +Hash+ instance into strings *Lucas Húngaro* + *Andrew White* -* Adds `Hash#deep_symbolize_keys` and `Hash#deep_symbolize_keys!` to convert all keys from a +Hash+ instance into symbols *Lucas Húngaro* +* Fix `ActiveSupport::Dependencies::Loadable#load_dependency` calling + `#blame_file!` on Exceptions that do not have the Blamable mixin -* `Object#try` can't call private methods. *Vasiliy Ermolovich* + *Andrew Kreiling* -* `AS::Callbacks#run_callbacks` remove `key` argument. *Francesco Rodriguez* +* Override `Time.at` to support the passing of Time-like values when called with a single argument. -* `deep_dup` works more expectedly now and duplicates also values in +Hash+ instances and elements in +Array+ instances. *Alexey Gaziev* + *Andrew White* -* Inflector no longer applies ice -> ouse to words like slice, police, ets *Wes Morgan* +* Prevent side effects to hashes inside arrays when + `Hash#with_indifferent_access` is called. -* Add `ActiveSupport::Deprecations.behavior = :silence` to completely ignore Rails runtime deprecations *twinturbo* + Fixes #10526 -* Make Module#delegate stop using `send` - can no longer delegate to private methods. *dasch* + *Yves Senn* -* AS::Callbacks: deprecate `:rescuable` option. *Bogdan Gusiev* +* Raise an error when multiple `included` blocks are defined for a Concern. + The old behavior would silently discard previously defined blocks, running + only the last one. -* Adds Integer#ordinal to get the ordinal suffix string of an integer. *Tim Gildea* + *Mike Dillon* -* AS::Callbacks: `:per_key` option is no longer supported +* Replace `multi_json` with `json`. -* `AS::Callbacks#define_callbacks`: add `:skip_after_callbacks_if_terminated` option. + Since Rails requires Ruby 1.9 and since Ruby 1.9 includes `json` in the standard library, + `multi_json` is no longer necessary. -* Add html_escape_once to ERB::Util, and delegate escape_once tag helper to it. *Carlos Antonio da Silva* + *Erik Michaels-Ober* -* Deprecates the compatibility method Module#local_constant_names, - use Module#local_constants instead (which returns symbols). *fxn* +* Added escaping of U+2028 and U+2029 inside the json encoder. + These characters are legal in JSON but break the Javascript interpreter. + After escaping them, the JSON is still legal and can be parsed by Javascript. -* Deletes the compatibility method Module#method_names, - use Module#methods from now on (which returns symbols). *fxn* + *Mario Caropreso + Viktor Kelemen + zackham* -* Deletes the compatibility method Module#instance_method_names, - use Module#instance_methods from now on (which returns symbols). *fxn* +* Fix skipping object callbacks using metadata fetched via callback chain + inspection methods (`_*_callbacks`) -* BufferedLogger is deprecated. Use ActiveSupport::Logger, or the logger - from Ruby stdlib. + *Sean Walbran* -* Unicode database updated to 6.1.0. +* Add a `fetch_multi` method to the cache stores. The method provides + an easy to use API for fetching multiple values from the cache. -* Adds `encode_big_decimal_as_string` option to force JSON serialization of BigDecimals as numeric instead - of wrapping them in strings for safety. + Example: -* Remove deprecated ActiveSupport::JSON::Variable. *Erich Menge* + # Calculating scores is expensive, so we only do it for posts + # that have been updated. Cache keys are automatically extracted + # from objects that define a #cache_key method. + scores = Rails.cache.fetch_multi(*posts) do |post| + calculate_score(post) + end -* Optimize log subscribers to check log level before doing any processing. *Brian Durand* + *Daniel Schierbeck* -Please check [3-2-stable](https://github.com/rails/rails/blob/3-2-stable/activesupport/CHANGELOG.md) for previous changes. +Please check [4-0-stable](https://github.com/rails/rails/blob/4-0-stable/activesupport/CHANGELOG.md) for previous changes. diff --git a/activesupport/MIT-LICENSE b/activesupport/MIT-LICENSE index c2bcf1d3e7..6aeeb7132d 100644 --- a/activesupport/MIT-LICENSE +++ b/activesupport/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2005-2012 David Heinemeier Hansson +Copyright (c) 2005-2013 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/README.rdoc b/activesupport/README.rdoc index ed688ecc59..f3582767c0 100644 --- a/activesupport/README.rdoc +++ b/activesupport/README.rdoc @@ -12,7 +12,7 @@ The latest version of Active Support can be installed with RubyGems: % [sudo] gem install activesupport -Source code can be downloaded as part of the Rails project on GitHub +Source code can be downloaded as part of the Rails project on GitHub: * https://github.com/rails/rails/tree/master/activesupport @@ -26,7 +26,7 @@ Active Support is released under the MIT license: == Support -API documentation is at +API documentation is at: * http://api.rubyonrails.org diff --git a/activesupport/Rakefile b/activesupport/Rakefile index 822c9d98ae..5ba153662a 100644 --- a/activesupport/Rakefile +++ b/activesupport/Rakefile @@ -9,22 +9,22 @@ Rake::TestTask.new do |t| t.verbose = true end + namespace :test do - Rake::TestTask.new(:isolated) do |t| - t.pattern = 'test/ts_isolated.rb' + task :isolated do + Dir.glob("test/**/*_test.rb").all? do |file| + sh(Gem.ruby, '-w', '-Ilib:test', file) + end or raise "Failures" end end -# Create compressed packages -dist_dirs = [ "lib", "test"] - spec = eval(File.read('activesupport.gemspec')) Gem::PackageTask.new(spec) do |p| p.gem_spec = spec end -desc "Release to gemcutter" +desc "Release to rubygems" task :release => :package do require 'rake/gemcutter' Rake::Gemcutter::Tasks.new(spec).define diff --git a/activesupport/activesupport.gemspec b/activesupport/activesupport.gemspec index 2c536cbd05..ffc2c2074e 100644 --- a/activesupport/activesupport.gemspec +++ b/activesupport/activesupport.gemspec @@ -15,13 +15,14 @@ Gem::Specification.new do |s| s.email = 'david@loudthinking.com' s.homepage = 'http://www.rubyonrails.org' - s.files = Dir['CHANGELOG.md', 'MIT-LICENSE', 'README.rdoc', 'lib/**/*'].select { |path| File.file? path } + s.files = Dir['CHANGELOG.md', 'MIT-LICENSE', 'README.rdoc', 'lib/**/*'] s.require_path = 'lib' s.rdoc_options.concat ['--encoding', 'UTF-8'] - s.add_dependency 'i18n', '~> 0.6' - s.add_dependency 'multi_json', '~> 1.3' - s.add_dependency 'tzinfo', '~> 0.3.33' - s.add_dependency 'minitest', '~> 4.1' + s.add_dependency('i18n', '~> 0.6', '>= 0.6.4') + s.add_dependency 'json', '~> 1.7' + s.add_dependency 'tzinfo', '~> 0.3.37' + s.add_dependency 'minitest', '~> 5.0' + s.add_dependency 'thread_safe','~> 0.1' end diff --git a/activesupport/bin/generate_tables b/activesupport/bin/generate_tables index 5fefa429df..f39e89b7d0 100755 --- a/activesupport/bin/generate_tables +++ b/activesupport/bin/generate_tables @@ -28,12 +28,6 @@ module ActiveSupport def initialize @ucd = Unicode::UnicodeDatabase.new - - default = Codepoint.new - default.combining_class = 0 - default.uppercase_mapping = 0 - default.lowercase_mapping = 0 - @ucd.codepoints = Hash.new(default) end def parse_codepoints(line) diff --git a/activesupport/lib/active_support.rb b/activesupport/lib/active_support.rb index b602686114..5e1fe9e556 100644 --- a/activesupport/lib/active_support.rb +++ b/activesupport/lib/active_support.rb @@ -1,5 +1,5 @@ #-- -# Copyright (c) 2005-2012 David Heinemeier Hansson +# Copyright (c) 2005-2013 David Heinemeier Hansson # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -39,7 +39,6 @@ module ActiveSupport eager_autoload do autoload :BacktraceCleaner - autoload :BasicObject autoload :ProxyObject autoload :Benchmarkable autoload :Cache diff --git a/activesupport/lib/active_support/all.rb b/activesupport/lib/active_support/all.rb index f537818300..151008bbaa 100644 --- a/activesupport/lib/active_support/all.rb +++ b/activesupport/lib/active_support/all.rb @@ -1,3 +1,4 @@ require 'active_support' +require 'active_support/deprecation' require 'active_support/time' require 'active_support/core_ext' diff --git a/activesupport/lib/active_support/backtrace_cleaner.rb b/activesupport/lib/active_support/backtrace_cleaner.rb index f1aff8a8e3..c88ae3e661 100644 --- a/activesupport/lib/active_support/backtrace_cleaner.rb +++ b/activesupport/lib/active_support/backtrace_cleaner.rb @@ -13,17 +13,17 @@ module ActiveSupport # can focus on the rest. # # bc = BacktraceCleaner.new - # bc.add_filter { |line| line.gsub(Rails.root, '') } - # bc.add_silencer { |line| line =~ /mongrel|rubygems/ } - # bc.clean(exception.backtrace) # will strip the Rails.root prefix and skip any lines from mongrel or rubygems + # bc.add_filter { |line| line.gsub(Rails.root, '') } # strip the Rails.root prefix + # bc.add_silencer { |line| line =~ /mongrel|rubygems/ } # skip any lines from mongrel or rubygems + # bc.clean(exception.backtrace) # perform the cleanup # # To reconfigure an existing BacktraceCleaner (like the default one in Rails) # and show as much data as possible, you can always call # <tt>BacktraceCleaner#remove_silencers!</tt>, which will restore the # backtrace to a pristine state. If you need to reconfigure an existing # BacktraceCleaner so that it does not filter or modify the paths of any lines - # of the backtrace, you can call BacktraceCleaner#remove_filters! These two - # methods will give you a completely untouched backtrace. + # of the backtrace, you can call <tt>BacktraceCleaner#remove_filters!<tt> + # These two methods will give you a completely untouched backtrace. # # Inspired by the Quiet Backtrace gem by Thoughtbot. class BacktraceCleaner @@ -72,6 +72,9 @@ module ActiveSupport @silencers = [] end + # Removes all filters, but leaves in silencers. Useful if you suddenly + # need to see entire filepaths in the backtrace that you had already + # filtered out. def remove_filters! @filters = [] end @@ -94,11 +97,7 @@ module ActiveSupport end def noise(backtrace) - @silencers.each do |s| - backtrace = backtrace.select { |line| s.call(line) } - end - - backtrace + backtrace - silence(backtrace) end end end diff --git a/activesupport/lib/active_support/basic_object.rb b/activesupport/lib/active_support/basic_object.rb deleted file mode 100644 index 242b766b58..0000000000 --- a/activesupport/lib/active_support/basic_object.rb +++ /dev/null @@ -1,7 +0,0 @@ -require 'active_support/deprecation' - -module ActiveSupport - # :nodoc: - # Deprecated in favor of ActiveSupport::ProxyObject - BasicObject = Deprecation::DeprecatedConstantProxy.new('ActiveSupport::BasicObject', 'ActiveSupport::ProxyObject') -end diff --git a/activesupport/lib/active_support/benchmarkable.rb b/activesupport/lib/active_support/benchmarkable.rb index 6413502b53..805b7a714f 100644 --- a/activesupport/lib/active_support/benchmarkable.rb +++ b/activesupport/lib/active_support/benchmarkable.rb @@ -45,15 +45,5 @@ module ActiveSupport yield end end - - # Silence the logger during the execution of the block. - def silence - message = "ActiveSupport::Benchmarkable#silence is deprecated. It will be removed from Rails 4.1." - ActiveSupport::Deprecation.warn message - old_logger_level, logger.level = logger.level, ::Logger::ERROR if logger - yield - ensure - logger.level = old_logger_level if logger - end end end diff --git a/activesupport/lib/active_support/buffered_logger.rb b/activesupport/lib/active_support/buffered_logger.rb deleted file mode 100644 index 0595446189..0000000000 --- a/activesupport/lib/active_support/buffered_logger.rb +++ /dev/null @@ -1,7 +0,0 @@ -require 'active_support/deprecation' -require 'active_support/logger' - -module ActiveSupport - BufferedLogger = ActiveSupport::Deprecation::DeprecatedConstantProxy.new( - 'BufferedLogger', '::ActiveSupport::Logger') -end diff --git a/activesupport/lib/active_support/cache.rb b/activesupport/lib/active_support/cache.rb index fdec2de1d5..c5633d902f 100644 --- a/activesupport/lib/active_support/cache.rb +++ b/activesupport/lib/active_support/cache.rb @@ -3,7 +3,6 @@ require 'zlib' require 'active_support/core_ext/array/extract_options' require 'active_support/core_ext/array/wrap' require 'active_support/core_ext/benchmark' -require 'active_support/core_ext/exception' require 'active_support/core_ext/class/attribute_accessors' require 'active_support/core_ext/numeric/bytes' require 'active_support/core_ext/numeric/time' @@ -57,16 +56,7 @@ module ActiveSupport case store when Symbol - store_class_name = store.to_s.camelize - store_class = - begin - require "active_support/cache/#{store}" - rescue LoadError => e - raise "Could not find cache store adapter for #{store} (#{e})" - else - ActiveSupport::Cache.const_get(store_class_name) - end - store_class.new(*parameters) + retrieve_store_class(store).new(*parameters) when nil ActiveSupport::Cache::MemoryStore.new else @@ -74,6 +64,18 @@ module ActiveSupport end end + # Expands out the +key+ argument into a key that can be used for the + # cache store. Optionally accepts a namespace, and all keys will be + # scoped within that namespace. + # + # If the +key+ argument provided is an array, or responds to +to_a+, then + # each of elements in the array will be turned into parameters/keys and + # concatenated into a single key. For example: + # + # expand_cache_key([:foo, :bar]) # => "foo/bar" + # expand_cache_key([:foo, :bar], "namespace") # => "namespace/foo/bar" + # + # The +key+ argument can also respond to +cache_key+ or +to_param+. def expand_cache_key(key, namespace = nil) expanded_cache_key = namespace ? "#{namespace}/" : "" @@ -95,6 +97,16 @@ module ActiveSupport else key.to_param end.to_s end + + # Obtains the specified cache store class, given the name of the +store+. + # Raises an error when the store class cannot be found. + def retrieve_store_class(store) + require "active_support/cache/#{store}" + rescue LoadError => e + raise "Could not find cache store adapter for #{store} (#{e})" + else + ActiveSupport::Cache.const_get(store.to_s.camelize) + end end # An abstract cache store class. There are multiple cache store @@ -216,13 +228,13 @@ module ActiveSupport # # Setting <tt>:race_condition_ttl</tt> is very useful in situations where # a cache entry is used very frequently and is under heavy load. If a - # cache expires and due to heavy load seven different processes will try + # cache expires and due to heavy load several different processes will try # to read data natively and then they all will try to write to cache. To # avoid that case the first process to find an expired cache entry will # bump the cache expiration time by the value set in <tt>:race_condition_ttl</tt>. # Yes, this process is extending the time for a stale value by another few # seconds. Because of extended life of the previous cache, other processes - # will continue to use slightly stale data for a just a big longer. In the + # will continue to use slightly stale data for a just a bit longer. In the # meantime that first process will go ahead and will write into cache the # new value. After that all the processes will start getting new value. # The key is to keep <tt>:race_condition_ttl</tt> small. @@ -276,34 +288,14 @@ module ActiveSupport if block_given? options = merged_options(options) key = namespaced_key(name, options) - unless options[:force] - entry = instrument(:read, name, options) do |payload| - payload[:super_operation] = :fetch if payload - read_entry(key, options) - end - end - if entry && entry.expired? - race_ttl = options[:race_condition_ttl].to_i - if race_ttl && (Time.now - entry.expires_at <= race_ttl) - # When an entry has :race_condition_ttl defined, put the stale entry back into the cache - # for a brief period while the entry is begin recalculated. - entry.expires_at = Time.now + race_ttl - write_entry(key, entry, :expires_in => race_ttl * 2) - else - delete_entry(key, options) - end - entry = nil - end + + cached_entry = find_cached_entry(key, name, options) unless options[:force] + entry = handle_expired_entry(cached_entry, key, options) if entry - instrument(:fetch_hit, name, options) { |payload| } - entry.value + get_entry_value(entry, name, options) else - result = instrument(:generate, name, options) do |payload| - yield(name) - end - write(name, result, options) - result + save_block_result_to_cache(name, options) { |_name| yield _name } end else read(name, options) @@ -360,12 +352,40 @@ module ActiveSupport results end + # Fetches data from the cache, using the given keys. If there is data in + # the cache with the given keys, then that data is returned. Otherwise, + # the supplied block is called for each key for which there was no data, + # and the result will be written to the cache and returned. + # + # Options are passed to the underlying cache implementation. + # + # Returns an array with the data for each of the names. For example: + # + # cache.write("bim", "bam") + # cache.fetch_multi("bim", "boom") {|key| key * 2 } + # #=> ["bam", "boomboom"] + # + def fetch_multi(*names) + options = names.extract_options! + options = merged_options(options) + + results = read_multi(*names, options) + + names.map do |name| + results.fetch(name) do + value = yield name + write(name, value, options) + value + end + end + end + # Writes the value to the cache, with the key. # # Options are passed to the underlying cache implementation. def write(name, value, options = nil) options = merged_options(options) - instrument(:write, name, options) do |payload| + instrument(:write, name, options) do entry = Entry.new(value, options) write_entry(namespaced_key(name, options), entry, options) end @@ -376,7 +396,7 @@ module ActiveSupport # Options are passed to the underlying cache implementation. def delete(name, options = nil) options = merged_options(options) - instrument(:delete, name) do |payload| + instrument(:delete, name) do delete_entry(namespaced_key(name, options), options) end end @@ -386,9 +406,9 @@ module ActiveSupport # Options are passed to the underlying cache implementation. def exist?(name, options = nil) options = merged_options(options) - instrument(:exist?, name) do |payload| + instrument(:exist?, name) do entry = read_entry(namespaced_key(name, options), options) - entry && !entry.expired? + (entry && !entry.expired?) || false end end @@ -532,6 +552,42 @@ module ActiveSupport return unless logger && logger.debug? && !silence? logger.debug("Cache #{operation}: #{key}#{options.blank? ? "" : " (#{options.inspect})"}") end + + def find_cached_entry(key, name, options) + instrument(:read, name, options) do |payload| + payload[:super_operation] = :fetch if payload + read_entry(key, options) + end + end + + def handle_expired_entry(entry, key, options) + if entry && entry.expired? + race_ttl = options[:race_condition_ttl].to_i + if race_ttl && (Time.now.to_f - entry.expires_at <= race_ttl) + # When an entry has :race_condition_ttl defined, put the stale entry back into the cache + # for a brief period while the entry is begin recalculated. + entry.expires_at = Time.now + race_ttl + write_entry(key, entry, :expires_in => race_ttl * 2) + else + delete_entry(key, options) + end + entry = nil + end + entry + end + + def get_entry_value(entry, name, options) + instrument(:fetch_hit, name, options) { |payload| } + entry.value + end + + def save_block_result_to_cache(name, options) + result = instrument(:generate, name, options) do |payload| + yield(name) + end + write(name, result, options) + result + end end # This class is used to represent cache entries. Cache entries have a value and an optional @@ -547,38 +603,38 @@ module ActiveSupport # +:compress+, +:compress_threshold+, and +:expires_in+. def initialize(value, options = {}) if should_compress?(value, options) - @v = compress(value) - @c = true + @value = compress(value) + @compressed = true else - @v = value - end - if expires_in = options[:expires_in] - @x = (Time.now + expires_in).to_i + @value = value end + @created_at = Time.now.to_f + @expires_in = options[:expires_in] + @expires_in = @expires_in.to_f if @expires_in end def value - convert_version_3_entry! if defined?(@value) - compressed? ? uncompress(@v) : @v + convert_version_4beta1_entry! if defined?(@v) + compressed? ? uncompress(@value) : @value end # Check if the entry is expired. The +expires_in+ parameter can override # the value set when the entry was created. def expired? - convert_version_3_entry! if defined?(@value) - if defined?(@x) - @x && @x < Time.now.to_i - else - false - end + convert_version_4beta1_entry! if defined?(@value) + @expires_in && @created_at + @expires_in <= Time.now.to_f end def expires_at - Time.at(@x) if defined?(@x) + @expires_in ? @created_at + @expires_in : nil end def expires_at=(value) - @x = value.to_i + if value + @expires_in = value.to_f - @created_at + else + @expires_in = nil + end end # Returns the size of the cached value. This could be less than @@ -591,9 +647,9 @@ module ActiveSupport when NilClass 0 when String - @v.bytesize + @value.bytesize else - @s = Marshal.dump(@v).bytesize + @s = Marshal.dump(@value).bytesize end end end @@ -601,12 +657,12 @@ module ActiveSupport # Duplicate the value in a class. This is used by cache implementations that don't natively # serialize entries to protect against accidental cache modifications. def dup_value! - convert_version_3_entry! if defined?(@value) - if @v && !compressed? && !(@v.is_a?(Numeric) || @v == true || @v == false) - if @v.is_a?(String) - @v = @v.dup + convert_version_4beta1_entry! if defined?(@v) + if @value && !compressed? && !(@value.is_a?(Numeric) || @value == true || @value == false) + if @value.is_a?(String) + @value = @value.dup else - @v = Marshal.load(Marshal.dump(@v)) + @value = Marshal.load(Marshal.dump(@value)) end end end @@ -622,7 +678,7 @@ module ActiveSupport end def compressed? - defined?(@c) ? @c : false + defined?(@compressed) ? @compressed : false end def compress(value) @@ -635,19 +691,19 @@ module ActiveSupport # The internals of this method changed between Rails 3.x and 4.0. This method provides the glue # to ensure that cache entries created under the old version still work with the new class definition. - def convert_version_3_entry! - if defined?(@value) - @v = @value - remove_instance_variable(:@value) + def convert_version_4beta1_entry! + if defined?(@v) + @value = @v + remove_instance_variable(:@v) end - if defined?(@compressed) - @c = @compressed - remove_instance_variable(:@compressed) + if defined?(@c) + @compressed = @c + remove_instance_variable(:@c) end - if defined?(@expires_in) && defined?(@created_at) && @expires_in && @created_at - @x = (@created_at + @expires_in).to_i - remove_instance_variable(:@created_at) - remove_instance_variable(:@expires_in) + if defined?(@x) && @x + @created_at ||= Time.now.to_f + @expires_in = @x - @created_at + remove_instance_variable(:@x) end end end diff --git a/activesupport/lib/active_support/cache/file_store.rb b/activesupport/lib/active_support/cache/file_store.rb index 8e265ad863..10d39463ec 100644 --- a/activesupport/lib/active_support/cache/file_store.rb +++ b/activesupport/lib/active_support/cache/file_store.rb @@ -22,19 +22,26 @@ module ActiveSupport extend Strategy::LocalCache 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 + # 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)} FileUtils.rm_r(root_dirs.collect{|f| File.join(cache_path, f)}) end + # Premptively iterates through all stored keys and removes the ones which have expired. def cleanup(options = nil) options = merged_options(options) - each_key(options) do |key| + search_dir(cache_path) do |fname| + key = file_path_key(fname) entry = read_entry(key, options) delete_entry(key, options) if entry && entry.expired? end end + # Increments an already existing integer value that is stored in the cache. + # If the key is not found nothing is done. def increment(name, amount = 1, options = nil) file_name = key_file_path(namespaced_key(name, options)) lock_file(file_name) do @@ -49,6 +56,8 @@ module ActiveSupport end end + # Decrements an already existing integer value that is stored in the cache. + # If the key is not found nothing is done. def decrement(name, amount = 1, options = nil) file_name = key_file_path(namespaced_key(name, options)) lock_file(file_name) do @@ -150,9 +159,9 @@ module ActiveSupport # Delete empty directories in the cache. def delete_empty_directories(dir) - return if dir == cache_path + return if File.realpath(dir) == File.realpath(cache_path) if Dir.entries(dir).reject {|f| EXCLUDED_DIRS.include?(f)}.empty? - File.delete(dir) rescue nil + Dir.delete(dir) rescue nil delete_empty_directories(File.dirname(dir)) 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 712db2c75a..512296554f 100644 --- a/activesupport/lib/active_support/cache/mem_cache_store.rb +++ b/activesupport/lib/active_support/cache/mem_cache_store.rb @@ -7,6 +7,7 @@ end require 'digest/md5' require 'active_support/core_ext/marshal' +require 'active_support/core_ext/array/extract_options' module ActiveSupport module Cache @@ -158,7 +159,7 @@ module ActiveSupport # characters properly. def escape_key(key) key = key.to_s.dup - key = key.force_encoding("BINARY") + key = key.force_encoding(Encoding::ASCII_8BIT) key = key.gsub(ESCAPE_KEY_CHARS){ |match| "%#{match.getbyte(0).to_s(16).upcase}" } key = "#{key[0, 213]}:md5:#{Digest::MD5.hexdigest(key)}" if key.size > 250 key diff --git a/activesupport/lib/active_support/cache/memory_store.rb b/activesupport/lib/active_support/cache/memory_store.rb index 4d26fb7e42..34ac91334a 100644 --- a/activesupport/lib/active_support/cache/memory_store.rb +++ b/activesupport/lib/active_support/cache/memory_store.rb @@ -36,6 +36,7 @@ module ActiveSupport end end + # Premptively iterates through all stored keys and removes the ones which have expired. def cleanup(options = nil) options = merged_options(options) instrument(:cleanup, :size => @data.size) do @@ -122,6 +123,13 @@ module ActiveSupport end protected + + PER_ENTRY_OVERHEAD = 240 + + def cached_size(key, entry) + key.to_s.bytesize + entry.size + PER_ENTRY_OVERHEAD + end + def read_entry(key, options) # :nodoc: entry = @data[key] synchronize do @@ -139,8 +147,11 @@ module ActiveSupport synchronize do old_entry = @data[key] return false if @data.key?(key) && options[:unless_exist] - @cache_size -= old_entry.size if old_entry - @cache_size += entry.size + if old_entry + @cache_size -= (old_entry.size - entry.size) + else + @cache_size += cached_size(key, entry) + end @key_access[key] = Time.now.to_f @data[key] = entry prune(@max_size * 0.75, @max_prune_time) if @cache_size > @max_size @@ -152,7 +163,7 @@ module ActiveSupport synchronize do @key_access.delete(key) entry = @data.delete(key) - @cache_size -= entry.size if entry + @cache_size -= cached_size(key, entry) if entry !!entry end end diff --git a/activesupport/lib/active_support/cache/strategy/local_cache.rb b/activesupport/lib/active_support/cache/strategy/local_cache.rb index db5f228a70..fb42c4a41e 100644 --- a/activesupport/lib/active_support/cache/strategy/local_cache.rb +++ b/activesupport/lib/active_support/cache/strategy/local_cache.rb @@ -8,6 +8,23 @@ module ActiveSupport # duration of a block. Repeated calls to the cache for the same key will hit the # in-memory cache for faster access. module LocalCache + # Class for storing and registering the local caches. + class LocalCacheRegistry # :nodoc: + extend ActiveSupport::PerThreadRegistry + + def initialize + @registry = {} + end + + def cache_for(local_cache_key) + @registry[local_cache_key] + end + + def set_cache_for(local_cache_key, value) + @registry[local_cache_key] = value + end + end + # Simple memory backed cache. This cache is not thread safe and is intended only # for serving as a temporary memory cache for a single thread. class LocalStore < Store @@ -41,24 +58,18 @@ module ActiveSupport # Use a local cache for the duration of block. def with_local_cache - save_val = Thread.current[thread_local_key] - begin - Thread.current[thread_local_key] = LocalStore.new - yield - ensure - Thread.current[thread_local_key] = save_val - end + use_temporary_local_cache(LocalStore.new) { yield } end #-- # This class wraps up local storage for middlewares. Only the middleware method should # construct them. class Middleware # :nodoc: - attr_reader :name, :thread_local_key + attr_reader :name, :local_cache_key - def initialize(name, thread_local_key) + def initialize(name, local_cache_key) @name = name - @thread_local_key = thread_local_key + @local_cache_key = local_cache_key @app = nil end @@ -68,10 +79,10 @@ module ActiveSupport end def call(env) - Thread.current[thread_local_key] = LocalStore.new + LocalCacheRegistry.set_cache_for(local_cache_key, LocalStore.new) @app.call(env) ensure - Thread.current[thread_local_key] = nil + LocalCacheRegistry.set_cache_for(local_cache_key, nil) end end @@ -80,7 +91,7 @@ module ActiveSupport def middleware @middleware ||= Middleware.new( "ActiveSupport::Cache::Strategy::LocalCache", - thread_local_key) + local_cache_key) end def clear(options = nil) # :nodoc: @@ -95,29 +106,13 @@ module ActiveSupport def increment(name, amount = 1, options = nil) # :nodoc: value = bypass_local_cache{super} - if local_cache - local_cache.mute do - if value - local_cache.write(name, value, options) - else - local_cache.delete(name, options) - end - end - end + increment_or_decrement(value, name, amount, options) value end def decrement(name, amount = 1, options = nil) # :nodoc: value = bypass_local_cache{super} - if local_cache - local_cache.mute do - if value - local_cache.write(name, value, options) - else - local_cache.delete(name, options) - end - end - end + increment_or_decrement(value, name, amount, options) value end @@ -146,21 +141,37 @@ module ActiveSupport end private - def thread_local_key - @thread_local_key ||= "#{self.class.name.underscore}_local_cache_#{object_id}".gsub(/[\/-]/, '_').to_sym + def increment_or_decrement(value, name, amount, options) + if local_cache + local_cache.mute do + if value + local_cache.write(name, value, options) + else + local_cache.delete(name, options) + end + end + end + end + + def local_cache_key + @local_cache_key ||= "#{self.class.name.underscore}_local_cache_#{object_id}".gsub(/[\/-]/, '_').to_sym end def local_cache - Thread.current[thread_local_key] + LocalCacheRegistry.cache_for(local_cache_key) end def bypass_local_cache - save_cache = Thread.current[thread_local_key] + use_temporary_local_cache(nil) { yield } + end + + def use_temporary_local_cache(temporary_cache) + save_cache = LocalCacheRegistry.cache_for(local_cache_key) begin - Thread.current[thread_local_key] = nil + LocalCacheRegistry.set_cache_for(local_cache_key, temporary_cache) yield ensure - Thread.current[thread_local_key] = save_cache + LocalCacheRegistry.set_cache_for(local_cache_key, save_cache) end end end diff --git a/activesupport/lib/active_support/callbacks.rb b/activesupport/lib/active_support/callbacks.rb index 3a8353857e..c3aac31323 100644 --- a/activesupport/lib/active_support/callbacks.rb +++ b/activesupport/lib/active_support/callbacks.rb @@ -1,8 +1,10 @@ require 'active_support/concern' require 'active_support/descendants_tracker' +require 'active_support/core_ext/array/extract_options' require 'active_support/core_ext/class/attribute' require 'active_support/core_ext/kernel/reporting' require 'active_support/core_ext/kernel/singleton_class' +require 'thread' module ActiveSupport # Callbacks are code hooks that are run at key points in an object's lifecycle. @@ -60,6 +62,8 @@ module ActiveSupport extend ActiveSupport::DescendantsTracker end + CALLBACK_FILTER_TYPES = [:before, :after, :around] + # Runs the callbacks for the given event. # # Calls the before and around callbacks in the order they were set, yields @@ -73,8 +77,14 @@ module ActiveSupport # save # end def run_callbacks(kind, &block) - runner_name = self.class.__define_callbacks(kind, self) - send(runner_name, &block) + cbs = send("_#{kind}_callbacks") + if cbs.empty? + yield if block_given? + else + runner = cbs.compile + e = Filters::Environment.new(self, false, nil, block) + runner.call(e).value + end end private @@ -85,155 +95,315 @@ module ActiveSupport def halted_callback_hook(filter) end - class Callback #:nodoc:# - @@_callback_sequence = 0 - - attr_accessor :chain, :filter, :kind, :options, :klass, :raw_filter + module Conditionals # :nodoc: + class Value + def initialize(&block) + @block = block + end + def call(target, value); @block.call(value); end + end + end - def initialize(chain, filter, kind, options, klass) - @chain, @kind, @klass = chain, kind, klass - deprecate_per_key_option(options) - normalize_options!(options) + module Filters + Environment = Struct.new(:target, :halted, :value, :run_block) - @raw_filter, @options = filter, options - @filter = _compile_filter(filter) - recompile_options! + class End + def call(env) + block = env.run_block + env.value = !env.halted && (!block || block.call) + env + end end + ENDING = End.new + + class Before + def self.build(next_callback, user_callback, user_conditions, chain_config, filter) + halted_lambda = chain_config[:terminator] + + if chain_config.key?(:terminator) && user_conditions.any? + halting_and_conditional(next_callback, user_callback, user_conditions, halted_lambda, filter) + elsif chain_config.key? :terminator + halting(next_callback, user_callback, halted_lambda, filter) + elsif user_conditions.any? + conditional(next_callback, user_callback, user_conditions) + else + simple next_callback, user_callback + end + end + + private - def deprecate_per_key_option(options) - if options[:per_key] - raise NotImplementedError, ":per_key option is no longer supported. Use generic :if and :unless options instead." + def self.halting_and_conditional(next_callback, user_callback, user_conditions, halted_lambda, filter) + lambda { |env| + target = env.target + value = env.value + halted = env.halted + + if !halted && user_conditions.all? { |c| c.call(target, value) } + result = user_callback.call target, value + env.halted = halted_lambda.call(target, result) + if env.halted + target.send :halted_callback_hook, filter + end + end + next_callback.call env + } end - end - def clone(chain, klass) - obj = super() - obj.chain = chain - obj.klass = klass - obj.options = @options.dup - obj.options[:if] = @options[:if].dup - obj.options[:unless] = @options[:unless].dup - obj + def self.halting(next_callback, user_callback, halted_lambda, filter) + lambda { |env| + target = env.target + value = env.value + halted = env.halted + + unless halted + result = user_callback.call target, value + env.halted = halted_lambda.call(target, result) + if env.halted + target.send :halted_callback_hook, filter + end + end + next_callback.call env + } + end + + def self.conditional(next_callback, user_callback, user_conditions) + lambda { |env| + target = env.target + value = env.value + + if user_conditions.all? { |c| c.call(target, value) } + user_callback.call target, value + end + next_callback.call env + } + end + + def self.simple(next_callback, user_callback) + lambda { |env| + user_callback.call env.target, env.value + next_callback.call env + } + end end - def normalize_options!(options) - options[:if] = Array(options[:if]) - options[:unless] = Array(options[:unless]) + class After + def self.build(next_callback, user_callback, user_conditions, chain_config) + if chain_config[:skip_after_callbacks_if_terminated] + if chain_config.key?(:terminator) && user_conditions.any? + halting_and_conditional(next_callback, user_callback, user_conditions) + elsif chain_config.key?(:terminator) + halting(next_callback, user_callback) + elsif user_conditions.any? + conditional next_callback, user_callback, user_conditions + else + simple next_callback, user_callback + end + else + if user_conditions.any? + conditional next_callback, user_callback, user_conditions + else + simple next_callback, user_callback + end + end + end + + private + + def self.halting_and_conditional(next_callback, user_callback, user_conditions) + lambda { |env| + env = next_callback.call env + target = env.target + value = env.value + halted = env.halted + + if !halted && user_conditions.all? { |c| c.call(target, value) } + user_callback.call target, value + end + env + } + end + + def self.halting(next_callback, user_callback) + lambda { |env| + env = next_callback.call env + unless env.halted + user_callback.call env.target, env.value + end + env + } + end + + def self.conditional(next_callback, user_callback, user_conditions) + lambda { |env| + env = next_callback.call env + target = env.target + value = env.value + + if user_conditions.all? { |c| c.call(target, value) } + user_callback.call target, value + end + env + } + end + + def self.simple(next_callback, user_callback) + lambda { |env| + env = next_callback.call env + user_callback.call env.target, env.value + env + } + end end - def name - chain.name + class Around + def self.build(next_callback, user_callback, user_conditions, chain_config) + if chain_config.key?(:terminator) && user_conditions.any? + halting_and_conditional(next_callback, user_callback, user_conditions) + elsif chain_config.key? :terminator + halting(next_callback, user_callback) + elsif user_conditions.any? + conditional(next_callback, user_callback, user_conditions) + else + simple(next_callback, user_callback) + end + end + + private + + def self.halting_and_conditional(next_callback, user_callback, user_conditions) + lambda { |env| + target = env.target + value = env.value + halted = env.halted + + if !halted && user_conditions.all? { |c| c.call(target, value) } + user_callback.call(target, value) { + env = next_callback.call env + env.value + } + env + else + next_callback.call env + end + } + end + + def self.halting(next_callback, user_callback) + lambda { |env| + target = env.target + value = env.value + + unless env.halted + user_callback.call(target, value) { + env = next_callback.call env + env.value + } + env + else + next_callback.call env + end + } + end + + def self.conditional(next_callback, user_callback, user_conditions) + lambda { |env| + target = env.target + value = env.value + + if user_conditions.all? { |c| c.call(target, value) } + user_callback.call(target, value) { + env = next_callback.call env + env.value + } + env + else + next_callback.call env + end + } + end + + def self.simple(next_callback, user_callback) + lambda { |env| + user_callback.call(env.target, env.value) { + env = next_callback.call env + env.value + } + env + } + end end + end - def next_id - @@_callback_sequence += 1 + class Callback #:nodoc:# + def self.build(chain, filter, kind, options) + new chain.name, filter, kind, options, chain.config end - def matches?(_kind, _filter) - @kind == _kind && @filter == _filter + attr_accessor :kind, :name + attr_reader :chain_config + + def initialize(name, filter, kind, options, chain_config) + @chain_config = chain_config + @name = name + @kind = kind + @filter = filter + @key = compute_identifier filter + @if = Array(options[:if]) + @unless = Array(options[:unless]) end - def _update_filter(filter_options, new_options) - filter_options[:if].concat(Array(new_options[:unless])) if new_options.key?(:unless) - filter_options[:unless].concat(Array(new_options[:if])) if new_options.key?(:if) + def filter; @key; end + def raw_filter; @filter; end + + def merge(chain, new_options) + options = { + :if => @if.dup, + :unless => @unless.dup + } + + options[:if].concat Array(new_options.fetch(:unless, [])) + options[:unless].concat Array(new_options.fetch(:if, [])) + + self.class.build chain, @filter, @kind, options end - def recompile!(_options) - deprecate_per_key_option(_options) - _update_filter(self.options, _options) + def matches?(_kind, _filter) + @kind == _kind && filter == _filter + end - recompile_options! + def duplicates?(other) + case @filter + when Symbol, String + matches?(other.kind, other.filter) + else + false + end end # Wraps code with filter - def apply(code) - case @kind + def apply(next_callback) + user_conditions = conditions_lambdas + user_callback = make_lambda @filter + + case kind when :before - <<-RUBY_EVAL - if !halted && #{@compiled_options} - # This double assignment is to prevent warnings in 1.9.3 as - # the `result` variable is not always used except if the - # terminator code refers to it. - result = result = #{@filter} - halted = (#{chain.config[:terminator]}) - if halted - halted_callback_hook(#{@raw_filter.inspect.inspect}) - end - end - #{code} - RUBY_EVAL + Filters::Before.build(next_callback, user_callback, user_conditions, chain_config, @filter) when :after - <<-RUBY_EVAL - #{code} - if #{!chain.config[:skip_after_callbacks_if_terminated] || "!halted"} && #{@compiled_options} - #{@filter} - end - RUBY_EVAL + Filters::After.build(next_callback, user_callback, user_conditions, chain_config) when :around - name = define_conditional_callback - <<-RUBY_EVAL - #{name}(halted) do - #{code} - value - end - RUBY_EVAL + Filters::Around.build(next_callback, user_callback, user_conditions, chain_config) end end private - # Compile around filters with conditions into proxy methods - # that contain the conditions. - # - # For `set_callback :save, :around, :filter_name, if: :condition': - # - # def _conditional_callback_save_17 - # if condition - # filter_name do - # yield self - # end - # else - # yield self - # end - # end - def define_conditional_callback - name = "_conditional_callback_#{@kind}_#{next_id}" - @klass.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1 - def #{name}(halted) - if #{@compiled_options} && !halted - #{@filter} do - yield self - end - else - yield self - end - end - RUBY_EVAL - name - end - - # Options support the same options as filters themselves (and support - # symbols, string, procs, and objects), so compile a conditional - # expression based on the options. - def recompile_options! - conditions = ["true"] - - unless options[:if].empty? - conditions << Array(_compile_filter(options[:if])) - end - - unless options[:unless].empty? - conditions << Array(_compile_filter(options[:unless])).map {|f| "!#{f}"} - end - - @compiled_options = conditions.flatten.join(" && ") + def invert_lambda(l) + lambda { |*args, &blk| !l.call(*args, &blk) } end # Filters support: # - # Arrays:: Used in conditions. This is used to specify - # multiple conditions. Used internally to - # merge conditions from skip_* filters. # Symbols:: A method to call. # Strings:: Some content to evaluate. # Procs:: A proc to call with the object. @@ -242,130 +412,153 @@ module ActiveSupport # All of these objects are compiled into methods and handled # the same after this point: # - # Arrays:: Merged together into a single filter. # Symbols:: Already methods. - # Strings:: class_eval'ed into methods. - # Procs:: define_method'ed into methods. + # Strings:: class_eval'd into methods. + # Procs:: using define_method compiled into methods. # Objects:: # a method is created that calls the before_foo method # on the object. - def _compile_filter(filter) - method_name = "_callback_#{@kind}_#{next_id}" + def make_lambda(filter) case filter - when Array - filter.map {|f| _compile_filter(f)} when Symbol - filter + lambda { |target, _, &blk| target.send filter, &blk } when String - "(#{filter})" - when Proc - @klass.send(:define_method, method_name, &filter) - return method_name if filter.arity <= 0 + l = eval "lambda { |value| #{filter} }" + lambda { |target, value| target.instance_exec(value, &l) } + when Conditionals::Value then filter + when ::Proc + if filter.arity > 1 + return lambda { |target, _, &block| + raise ArgumentError unless block + target.instance_exec(target, block, &filter) + } + end - method_name << (filter.arity == 1 ? "(self)" : " self, Proc.new ") + if filter.arity <= 0 + lambda { |target, _| target.instance_exec(&filter) } + else + lambda { |target, _| target.instance_exec(target, &filter) } + end else - @klass.send(:define_method, "#{method_name}_object") { filter } + scopes = Array(chain_config[:scope]) + method_to_call = scopes.map{ |s| public_send(s) }.join("_") - _normalize_legacy_filter(kind, filter) - scopes = Array(chain.config[:scope]) - method_to_call = scopes.map{ |s| s.is_a?(Symbol) ? send(s) : s }.join("_") - - @klass.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1 - def #{method_name}(&blk) - #{method_name}_object.send(:#{method_to_call}, self, &blk) - end - RUBY_EVAL - - method_name + lambda { |target, _, &blk| + filter.public_send method_to_call, target, &blk + } end end - def _normalize_legacy_filter(kind, filter) - if !filter.respond_to?(kind) && filter.respond_to?(:filter) - message = "Filter object with #filter method is deprecated. Define method corresponding " \ - "to filter type (#before, #after or #around)." - ActiveSupport::Deprecation.warn message - filter.singleton_class.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1 - def #{kind}(context, &block) filter(context, &block) end - RUBY_EVAL - elsif filter.respond_to?(:before) && filter.respond_to?(:after) && kind == :around && !filter.respond_to?(:around) - message = "Filter object with #before and #after methods is deprecated. Define #around method instead." - ActiveSupport::Deprecation.warn message - def filter.around(context) - should_continue = before(context) - yield if should_continue - after(context) - end + def compute_identifier(filter) + case filter + when String, ::Proc + filter.object_id + else + filter end end + + def conditions_lambdas + @if.map { |c| make_lambda c } + + @unless.map { |c| invert_lambda make_lambda c } + end end # An Array with a compile method. - class CallbackChain < Array #:nodoc:# + class CallbackChain #:nodoc:# + include Enumerable + attr_reader :name, :config def initialize(name, config) @name = name @config = { - :terminator => "false", :scope => [ :kind ] - }.merge(config) + }.merge!(config) + @chain = [] + @callbacks = nil + @mutex = Mutex.new end - def compile - method = [] - method << "value = nil" - method << "halted = false" + def each(&block); @chain.each(&block); end + def index(o); @chain.index(o); end + def empty?; @chain.empty?; end - callbacks = "value = !halted && (!block_given? || yield)" - reverse_each do |callback| - callbacks = callback.apply(callbacks) - end - method << callbacks + def insert(index, o) + @callbacks = nil + @chain.insert(index, o) + end - method << "value" - method.join("\n") + def delete(o) + @callbacks = nil + @chain.delete(o) end - end + def clear + @callbacks = nil + @chain.clear + self + end - module ClassMethods + def initialize_copy(other) + @callbacks = nil + @chain = other.chain.dup + @mutex = Mutex.new + end - # This method defines callback chain method for the given kind - # if it was not yet defined. - # This generated method plays caching role. - def __define_callbacks(kind, object) #:nodoc: - name = __callback_runner_name(kind) - unless object.respond_to?(name, true) - str = object.send("_#{kind}_callbacks").compile - class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1 - def #{name}() #{str} end - protected :#{name} - RUBY_EVAL + def compile + @callbacks || @mutex.synchronize do + @callbacks ||= @chain.reverse.inject(Filters::ENDING) do |chain, callback| + callback.apply chain + end end - name end - def __reset_runner(symbol) - name = __callback_runner_name(symbol) - undef_method(name) if method_defined?(name) + def append(*callbacks) + callbacks.each { |c| append_one(c) } end - def __callback_runner_name(kind) - "_run__#{self.name.hash.abs}__#{kind}__callbacks" + def prepend(*callbacks) + callbacks.each { |c| prepend_one(c) } end - # This is used internally to append, prepend and skip callbacks to the - # CallbackChain. - def __update_callbacks(name, filters = [], block = nil) #:nodoc: - type = [:before, :after, :around].include?(filters.first) ? filters.shift : :before - options = filters.last.is_a?(Hash) ? filters.pop : {} + protected + def chain; @chain; end + + private + + def append_one(callback) + @callbacks = nil + remove_duplicates(callback) + @chain.push(callback) + end + + def prepend_one(callback) + @callbacks = nil + remove_duplicates(callback) + @chain.unshift(callback) + end + + def remove_duplicates(callback) + @callbacks = nil + @chain.delete_if { |c| callback.duplicates?(c) } + end + end + + module ClassMethods + def normalize_callback_params(filters, block) # :nodoc: + type = CALLBACK_FILTER_TYPES.include?(filters.first) ? filters.shift : :before + options = filters.extract_options! filters.unshift(block) if block + [type, filters, options.dup] + end + # This is used internally to append, prepend and skip callbacks to the + # CallbackChain. + def __update_callbacks(name) #:nodoc: ([self] + ActiveSupport::DescendantsTracker.descendants(self)).reverse.each do |target| - chain = target.send("_#{name}_callbacks") - yield target, chain.dup, type, filters, options - target.__reset_runner(name) + chain = target.get_callbacks name + yield target, chain.dup end end @@ -381,7 +574,7 @@ module ActiveSupport # # set_callback :save, :before_meth # - # The callback can specified as a symbol naming an instance method; as a + # The callback can be specified as a symbol naming an instance method; as a # proc, lambda, or block; as a string to be instance evaluated; or as an # object that responds to a certain method determined by the <tt>:scope</tt> # argument to +define_callback+. @@ -405,20 +598,15 @@ module ActiveSupport # * <tt>:prepend</tt> - If +true+, the callback will be prepended to the # existing chain rather than appended. def set_callback(name, *filter_list, &block) - mapped = nil - - __update_callbacks(name, filter_list, block) do |target, chain, type, filters, options| - mapped ||= filters.map do |filter| - Callback.new(chain, filter, type, options.dup, self) - end - - filters.each do |filter| - chain.delete_if {|c| c.matches?(type, filter) } - end - - options[:prepend] ? chain.unshift(*(mapped.reverse)) : chain.push(*mapped) + type, filters, options = normalize_callback_params(filter_list, block) + self_chain = get_callbacks name + mapped = filters.map do |filter| + Callback.build(self_chain, filter, type, options) + end - target.send("_#{name}_callbacks=", chain) + __update_callbacks(name) do |target, chain| + options[:prepend] ? chain.prepend(*mapped) : chain.append(*mapped) + target.set_callbacks name, chain end end @@ -430,36 +618,34 @@ module ActiveSupport # skip_callback :validate, :before, :check_membership, if: -> { self.age > 18 } # end def skip_callback(name, *filter_list, &block) - __update_callbacks(name, filter_list, block) do |target, chain, type, filters, options| + type, filters, options = normalize_callback_params(filter_list, block) + + __update_callbacks(name) do |target, chain| filters.each do |filter| filter = chain.find {|c| c.matches?(type, filter) } if filter && options.any? - new_filter = filter.clone(chain, self) + new_filter = filter.merge(chain, options) chain.insert(chain.index(filter), new_filter) - new_filter.recompile!(options) end chain.delete(filter) end - target.send("_#{name}_callbacks=", chain) + target.set_callbacks name, chain end end # Remove all set callbacks for the given event. - def reset_callbacks(symbol) - callbacks = send("_#{symbol}_callbacks") + def reset_callbacks(name) + callbacks = get_callbacks name ActiveSupport::DescendantsTracker.descendants(self).each do |target| - chain = target.send("_#{symbol}_callbacks").dup + chain = target.get_callbacks(name).dup callbacks.each { |c| chain.delete(c) } - target.send("_#{symbol}_callbacks=", chain) - target.__reset_runner(symbol) + target.set_callbacks name, chain end - self.send("_#{symbol}_callbacks=", callbacks.dup.clear) - - __reset_runner(symbol) + self.set_callbacks name, callbacks.dup.clear end # Define sets of events in the object lifecycle that support callbacks. @@ -471,10 +657,11 @@ module ActiveSupport # # * <tt>:terminator</tt> - Determines when a before filter will halt the # callback chain, preventing following callbacks from being called and - # the event from being triggered. This is a string to be eval'ed. The - # result of the callback is available in the +result+ variable. + # the event from being triggered. This should be a lambda to be executed. + # The current object and the return result of the callback will be called + # with the lambda. # - # define_callbacks :validate, terminator: 'result == false' + # define_callbacks :validate, terminator: ->(target, result) { result == false } # # In this example, if any before validate callbacks returns +false+, # other callbacks are not executed. Defaults to +false+, meaning no value @@ -529,13 +716,30 @@ module ActiveSupport # define_callbacks :save, scope: [:name] # # would call <tt>Audit#save</tt>. - def define_callbacks(*callbacks) - config = callbacks.last.is_a?(Hash) ? callbacks.pop : {} - callbacks.each do |callback| - class_attribute "_#{callback}_callbacks" - send("_#{callback}_callbacks=", CallbackChain.new(callback, config)) + def define_callbacks(*names) + options = names.extract_options! + if options.key?(:terminator) && String === options[:terminator] + ActiveSupport::Deprecation.warn "String based terminators are deprecated, please use a lambda" + value = options[:terminator] + line = class_eval "lambda { |result| #{value} }", __FILE__, __LINE__ + options[:terminator] = lambda { |target, result| target.instance_exec(result, &line) } + end + + names.each do |name| + class_attribute "_#{name}_callbacks" + set_callbacks name, CallbackChain.new(name, options) end end + + protected + + def get_callbacks(name) + send "_#{name}_callbacks" + end + + def set_callbacks(name, callbacks) + send "_#{name}_callbacks=", callbacks + end end end end diff --git a/activesupport/lib/active_support/concern.rb b/activesupport/lib/active_support/concern.rb index eeeba60839..b796d01dfd 100644 --- a/activesupport/lib/active_support/concern.rb +++ b/activesupport/lib/active_support/concern.rb @@ -98,25 +98,33 @@ module ActiveSupport # include Bar # works, Bar takes care now of its dependencies # end module Concern + class MultipleIncludedBlocks < StandardError #:nodoc: + def initialize + super "Cannot define multiple 'included' blocks for a Concern" + end + end + def self.extended(base) #:nodoc: - base.instance_variable_set("@_dependencies", []) + base.instance_variable_set(:@_dependencies, []) end def append_features(base) - if base.instance_variable_defined?("@_dependencies") - base.instance_variable_get("@_dependencies") << self + if base.instance_variable_defined?(:@_dependencies) + base.instance_variable_get(:@_dependencies) << self return false else return false if base < self @_dependencies.each { |dep| base.send(:include, dep) } super - base.extend const_get("ClassMethods") if const_defined?("ClassMethods") - base.class_eval(&@_included_block) if instance_variable_defined?("@_included_block") + base.extend const_get(:ClassMethods) if const_defined?(:ClassMethods) + base.class_eval(&@_included_block) if instance_variable_defined?(:@_included_block) end end def included(base = nil, &block) if base.nil? + raise MultipleIncludedBlocks if instance_variable_defined?(:@_included_block) + @_included_block = block else super diff --git a/activesupport/lib/active_support/core_ext.rb b/activesupport/lib/active_support/core_ext.rb index b48bdf08e8..199aa91020 100644 --- a/activesupport/lib/active_support/core_ext.rb +++ b/activesupport/lib/active_support/core_ext.rb @@ -1,4 +1,3 @@ -Dir["#{File.dirname(__FILE__)}/core_ext/*.rb"].sort.each do |path| - next if File.basename(path, '.rb') == 'logger' - require "active_support/core_ext/#{File.basename(path, '.rb')}" +Dir["#{File.dirname(__FILE__)}/core_ext/*.rb"].each do |path| + require path end diff --git a/activesupport/lib/active_support/core_ext/array.rb b/activesupport/lib/active_support/core_ext/array.rb index 79ba79192a..7d0c1e4c8d 100644 --- a/activesupport/lib/active_support/core_ext/array.rb +++ b/activesupport/lib/active_support/core_ext/array.rb @@ -1,6 +1,5 @@ require 'active_support/core_ext/array/wrap' require 'active_support/core_ext/array/access' -require 'active_support/core_ext/array/uniq_by' require 'active_support/core_ext/array/conversions' require 'active_support/core_ext/array/extract_options' require 'active_support/core_ext/array/grouping' diff --git a/activesupport/lib/active_support/core_ext/array/access.rb b/activesupport/lib/active_support/core_ext/array/access.rb index a8f9dddae5..4f1e432b61 100644 --- a/activesupport/lib/active_support/core_ext/array/access.rb +++ b/activesupport/lib/active_support/core_ext/array/access.rb @@ -21,28 +21,28 @@ class Array # Equal to <tt>self[1]</tt>. # - # %w( a b c d e).second # => "b" + # %w( a b c d e ).second # => "b" def second self[1] end # Equal to <tt>self[2]</tt>. # - # %w( a b c d e).third # => "c" + # %w( a b c d e ).third # => "c" def third self[2] end # Equal to <tt>self[3]</tt>. # - # %w( a b c d e).fourth # => "d" + # %w( a b c d e ).fourth # => "d" def fourth self[3] end # Equal to <tt>self[4]</tt>. # - # %w( a b c d e).fifth # => "e" + # %w( a b c d e ).fifth # => "e" def fifth self[4] end diff --git a/activesupport/lib/active_support/core_ext/array/conversions.rb b/activesupport/lib/active_support/core_ext/array/conversions.rb index 64e9945ef5..76ffd23ed1 100644 --- a/activesupport/lib/active_support/core_ext/array/conversions.rb +++ b/activesupport/lib/active_support/core_ext/array/conversions.rb @@ -8,11 +8,11 @@ class Array # Converts the array to a comma-separated sentence where the last element is # joined by the connector word. # - # You can pass the following options to change the default behaviour. If you + # You can pass the following options to change the default behavior. If you # pass an option key that doesn't exist in the list below, it will raise an # <tt>ArgumentError</tt>. # - # Options: + # ==== Options # # * <tt>:words_connector</tt> - The sign or word used to join the elements # in arrays with two or more elements (default: ", "). @@ -24,6 +24,8 @@ class Array # the connector options defined on the 'support.array' namespace in the # corresponding dictionary file. # + # ==== Examples + # # [].to_sentence # => "" # ['one'].to_sentence # => "one" # ['one', 'two'].to_sentence # => "one and two" @@ -38,10 +40,10 @@ class Array # ['one', 'two', 'three'].to_sentence(words_connector: ' or ', last_word_connector: ' or at least ') # # => "one or two or at least three" # - # Examples using <tt>:locale</tt> option: + # Using <tt>:locale</tt> option: # # # Given this locale dictionary: - # # + # # # # es: # # support: # # array: @@ -80,23 +82,8 @@ class Array end end - # Converts a collection of elements into a formatted string by calling - # <tt>to_s</tt> on all elements and joining them. Having this model: - # - # class Blog < ActiveRecord::Base - # def to_s - # title - # end - # end - # - # Blog.all.map(&:title) #=> ["First Post", "Second Post", "Third post"] - # - # <tt>to_formatted_s</tt> shows us: - # - # Blog.all.to_formatted_s # => "First PostSecond PostThird Post" - # - # Adding in the <tt>:db</tt> argument as the format yields a comma separated - # id list: + # Extends <tt>Array#to_s</tt> to convert a collection of elements into a + # comma separated id list if <tt>:db</tt> argument is given as the format. # # Blog.all.to_formatted_s(:db) # => "1,2,3" def to_formatted_s(format = :default) diff --git a/activesupport/lib/active_support/core_ext/array/grouping.rb b/activesupport/lib/active_support/core_ext/array/grouping.rb index 640e6e9328..37f007c751 100644 --- a/activesupport/lib/active_support/core_ext/array/grouping.rb +++ b/activesupport/lib/active_support/core_ext/array/grouping.rb @@ -25,15 +25,13 @@ class Array # subtracting from number gives how many to add; # modulo number ensures we don't add group of just fill. padding = (number - size % number) % number - collection = dup.concat([fill_with] * padding) + collection = dup.concat(Array.new(padding, fill_with)) end if block_given? collection.each_slice(number) { |slice| yield(slice) } else - groups = [] - collection.each_slice(number) { |group| groups << group } - groups + collection.each_slice(number).to_a end end @@ -86,13 +84,27 @@ class Array # [1, 2, 3, 4, 5].split(3) # => [[1, 2], [4, 5]] # (1..10).to_a.split { |i| i % 3 == 0 } # => [[1, 2], [4, 5], [7, 8], [10]] def split(value = nil, &block) - inject([[]]) do |results, element| - if block && block.call(element) || value == element - results << [] - else - results.last << element - end + if block + inject([[]]) do |results, element| + if block.call(element) + results << [] + else + results.last << element + end + results + end + else + results, arr = [[]], self + until arr.empty? + if (idx = index(value)) + results.last.concat(arr.shift(idx)) + arr.shift + results << [] + else + results.last.concat(arr.shift(arr.size)) + end + end results end end diff --git a/activesupport/lib/active_support/core_ext/array/uniq_by.rb b/activesupport/lib/active_support/core_ext/array/uniq_by.rb deleted file mode 100644 index ca3b7748cd..0000000000 --- a/activesupport/lib/active_support/core_ext/array/uniq_by.rb +++ /dev/null @@ -1,19 +0,0 @@ -class Array - # *DEPRECATED*: Use +Array#uniq+ instead. - # - # Returns a unique array based on the criteria in the block. - # - # [1, 2, 3, 4].uniq_by { |i| i.odd? } # => [1, 2] - def uniq_by(&block) - ActiveSupport::Deprecation.warn 'uniq_by is deprecated. Use Array#uniq instead' - uniq(&block) - end - - # *DEPRECATED*: Use +Array#uniq!+ instead. - # - # Same as +uniq_by+, but modifies +self+. - def uniq_by!(&block) - ActiveSupport::Deprecation.warn 'uniq_by! is deprecated. Use Array#uniq! instead' - uniq!(&block) - end -end diff --git a/activesupport/lib/active_support/core_ext/array/wrap.rb b/activesupport/lib/active_support/core_ext/array/wrap.rb index 05b09a4c7f..152eb02218 100644 --- a/activesupport/lib/active_support/core_ext/array/wrap.rb +++ b/activesupport/lib/active_support/core_ext/array/wrap.rb @@ -15,12 +15,12 @@ class Array # # * If the argument responds to +to_ary+ the method is invoked. <tt>Kernel#Array</tt> # moves on to try +to_a+ if the returned value is +nil+, but <tt>Array.wrap</tt> returns - # such a +nil+ right away. + # +nil+ right away. # * If the returned value from +to_ary+ is neither +nil+ nor an +Array+ object, <tt>Kernel#Array</tt> # raises an exception, while <tt>Array.wrap</tt> does not, it just returns the value. - # * It does not call +to_a+ on the argument, though special-cases +nil+ to return an empty array. + # * It does not call +to_a+ on the argument, but returns an empty array if argument is +nil+. # - # The last point is particularly worth comparing for some enumerables: + # The second point is easily explained with some enumerables: # # Array(foo: :bar) # => [[:foo, :bar]] # Array.wrap(foo: :bar) # => [{:foo=>:bar}] @@ -29,11 +29,10 @@ class Array # # [*object] # - # which for +nil+ returns <tt>[nil]</tt> (Ruby 1.8.7) or <tt>[]</tt> (Ruby - # 1.9), and calls to <tt>Array(object)</tt> otherwise. + # which returns <tt>[]</tt> for +nil+, but calls to <tt>Array(object)</tt> otherwise. # - # Thus, in this case the behavior may be different for +nil+, and the differences with - # <tt>Kernel#Array</tt> explained above apply to the rest of <tt>object</tt>s. + # The differences with <tt>Kernel#Array</tt> explained above + # apply to the rest of <tt>object</tt>s. def self.wrap(object) if object.nil? [] diff --git a/activesupport/lib/active_support/core_ext/benchmark.rb b/activesupport/lib/active_support/core_ext/benchmark.rb index 2d110155a5..eb25b2bc44 100644 --- a/activesupport/lib/active_support/core_ext/benchmark.rb +++ b/activesupport/lib/active_support/core_ext/benchmark.rb @@ -1,6 +1,13 @@ require 'benchmark' class << Benchmark + # Benchmark realtime in milliseconds. + # + # Benchmark.realtime { User.all } + # # => 8.0e-05 + # + # Benchmark.ms { User.all } + # # => 0.074 def ms 1000 * realtime { yield } end diff --git a/activesupport/lib/active_support/core_ext/big_decimal/conversions.rb b/activesupport/lib/active_support/core_ext/big_decimal/conversions.rb index 5dc5710c53..39b8cea807 100644 --- a/activesupport/lib/active_support/core_ext/big_decimal/conversions.rb +++ b/activesupport/lib/active_support/core_ext/big_decimal/conversions.rb @@ -1,4 +1,5 @@ require 'bigdecimal' +require 'bigdecimal/util' require 'yaml' class BigDecimal diff --git a/activesupport/lib/active_support/core_ext/class/attribute.rb b/activesupport/lib/active_support/core_ext/class/attribute.rb index 1504e18839..f2a221c396 100644 --- a/activesupport/lib/active_support/core_ext/class/attribute.rb +++ b/activesupport/lib/active_support/core_ext/class/attribute.rb @@ -44,7 +44,8 @@ class Class # Base.setting # => [] # Subclass.setting # => [:foo] # - # For convenience, a query method is defined as well: + # For convenience, an instance predicate method is defined as well. + # To skip it, pass <tt>instance_predicate: false</tt>. # # Subclass.setting? # => false # @@ -71,50 +72,56 @@ class Class options = attrs.extract_options! instance_reader = options.fetch(:instance_accessor, true) && options.fetch(:instance_reader, true) instance_writer = options.fetch(:instance_accessor, true) && options.fetch(:instance_writer, true) + instance_predicate = options.fetch(:instance_predicate, true) - # We use class_eval here rather than define_method because class_attribute - # may be used in a performance sensitive context therefore the overhead that - # define_method introduces may become significant. attrs.each do |name| - class_eval <<-RUBY, __FILE__, __LINE__ + 1 - def self.#{name}() nil end - def self.#{name}?() !!#{name} end + define_singleton_method(name) { nil } + define_singleton_method("#{name}?") { !!public_send(name) } if instance_predicate - def self.#{name}=(val) - singleton_class.class_eval do - remove_possible_method(:#{name}) - define_method(:#{name}) { val } - end + ivar = "@#{name}" + + define_singleton_method("#{name}=") do |val| + singleton_class.class_eval do + remove_possible_method(name) + define_method(name) { val } + end - if singleton_class? - class_eval do - remove_possible_method(:#{name}) - def #{name} - defined?(@#{name}) ? @#{name} : singleton_class.#{name} + if singleton_class? + class_eval do + remove_possible_method(name) + define_method(name) do + if instance_variable_defined? ivar + instance_variable_get ivar + else + singleton_class.send name end end end - val end + val + end - if instance_reader - remove_possible_method :#{name} - def #{name} - defined?(@#{name}) ? @#{name} : self.class.#{name} - end - - def #{name}? - !!#{name} + if instance_reader + remove_possible_method name + define_method(name) do + if instance_variable_defined?(ivar) + instance_variable_get ivar + else + self.class.public_send name end end - RUBY + define_method("#{name}?") { !!public_send(name) } if instance_predicate + end attr_writer name if instance_writer end end private - def singleton_class? - ancestors.first != self + + unless respond_to?(:singleton_class?) + def singleton_class? + ancestors.first != self + end end end diff --git a/activesupport/lib/active_support/core_ext/class/attribute_accessors.rb b/activesupport/lib/active_support/core_ext/class/attribute_accessors.rb index fa1dbfdf06..34859617c9 100644 --- a/activesupport/lib/active_support/core_ext/class/attribute_accessors.rb +++ b/activesupport/lib/active_support/core_ext/class/attribute_accessors.rb @@ -32,7 +32,7 @@ class Class def cattr_reader(*syms) options = syms.extract_options! syms.each do |sym| - raise NameError.new('invalid attribute name') unless sym =~ /^[_A-Za-z]\w*$/ + raise NameError.new("invalid class attribute name: #{sym}") unless sym =~ /^[_A-Za-z]\w*$/ class_eval(<<-EOS, __FILE__, __LINE__ + 1) unless defined? @@#{sym} @@#{sym} = nil @@ -93,7 +93,7 @@ class Class def cattr_writer(*syms) options = syms.extract_options! syms.each do |sym| - raise NameError.new('invalid attribute name') unless sym =~ /^[_A-Za-z]\w*$/ + raise NameError.new("invalid class attribute name: #{sym}") unless sym =~ /^[_A-Za-z]\w*$/ class_eval(<<-EOS, __FILE__, __LINE__ + 1) unless defined? @@#{sym} @@#{sym} = nil diff --git a/activesupport/lib/active_support/core_ext/class/subclasses.rb b/activesupport/lib/active_support/core_ext/class/subclasses.rb index 9a2dc6e7c5..3c4bfc5f1e 100644 --- a/activesupport/lib/active_support/core_ext/class/subclasses.rb +++ b/activesupport/lib/active_support/core_ext/class/subclasses.rb @@ -29,9 +29,9 @@ class Class # # class Foo; end # class Bar < Foo; end - # class Baz < Foo; end + # class Baz < Bar; end # - # Foo.subclasses # => [Baz, Bar] + # Foo.subclasses # => [Bar] def subclasses subclasses, chain = [], descendants chain.each do |k| diff --git a/activesupport/lib/active_support/core_ext/date/calculations.rb b/activesupport/lib/active_support/core_ext/date/calculations.rb index 439d380af7..c60e833441 100644 --- a/activesupport/lib/active_support/core_ext/date/calculations.rb +++ b/activesupport/lib/active_support/core_ext/date/calculations.rb @@ -8,8 +8,6 @@ require 'active_support/core_ext/date_and_time/calculations' class Date include DateAndTime::Calculations - @beginning_of_week_default = nil - class << self attr_accessor :beginning_of_week_default @@ -53,28 +51,39 @@ class Date # Converts Date to a Time (or DateTime if necessary) with the time portion set to the beginning of the day (0:00) # and then subtracts the specified number of seconds. def ago(seconds) - to_time_in_current_zone.since(-seconds) + in_time_zone.since(-seconds) end # Converts Date to a Time (or DateTime if necessary) with the time portion set to the beginning of the day (0:00) # and then adds the specified number of seconds def since(seconds) - to_time_in_current_zone.since(seconds) + in_time_zone.since(seconds) end alias :in :since # Converts Date to a Time (or DateTime if necessary) with the time portion set to the beginning of the day (0:00) def beginning_of_day - to_time_in_current_zone + in_time_zone end alias :midnight :beginning_of_day alias :at_midnight :beginning_of_day alias :at_beginning_of_day :beginning_of_day + # Converts Date to a Time (or DateTime if necessary) with the time portion set to the middle of the day (12:00) + def middle_of_day + in_time_zone.middle_of_day + end + alias :midday :middle_of_day + alias :noon :middle_of_day + alias :at_midday :middle_of_day + alias :at_noon :middle_of_day + alias :at_middle_of_day :middle_of_day + # Converts Date to a Time (or DateTime if necessary) with the time portion set to the end of the day (23:59:59) def end_of_day - to_time_in_current_zone.end_of_day + in_time_zone.end_of_day end + alias :at_end_of_day :end_of_day def plus_with_duration(other) #:nodoc: if ActiveSupport::Duration === other @@ -120,4 +129,15 @@ class Date options.fetch(:day, day) ) end + + # Allow Date to be compared with Time by converting to DateTime and relying on the <=> from there. + def compare_with_coercion(other) + if other.is_a?(Time) + self.to_datetime <=> other + else + compare_without_coercion(other) + end + end + alias_method :compare_without_coercion, :<=> + alias_method :<=>, :compare_with_coercion end diff --git a/activesupport/lib/active_support/core_ext/date/conversions.rb b/activesupport/lib/active_support/core_ext/date/conversions.rb index 9120b0ba49..6bc8f12176 100644 --- a/activesupport/lib/active_support/core_ext/date/conversions.rb +++ b/activesupport/lib/active_support/core_ext/date/conversions.rb @@ -1,7 +1,6 @@ require 'date' require 'active_support/inflector/methods' require 'active_support/core_ext/date/zones' -require 'active_support/core_ext/module/remove_method' class Date DATE_FORMATS = { @@ -13,14 +12,15 @@ class Date day_format = ActiveSupport::Inflector.ordinalize(date.day) date.strftime("%B #{day_format}, %Y") # => "April 25th, 2007" }, - :rfc822 => '%e %b %Y' + :rfc822 => '%e %b %Y', + :iso8601 => lambda { |date| date.iso8601 } } # Ruby 1.9 has Date#to_time which converts to localtime only. - remove_possible_method :to_time + remove_method :to_time # Ruby 1.9 has Date#xmlschema which converts to a string without the time component. - remove_possible_method :xmlschema + remove_method :xmlschema # Convert to a formatted string. See DATE_FORMATS for predefined formats. # @@ -35,6 +35,7 @@ class Date # date.to_formatted_s(:long) # => "November 10, 2007" # date.to_formatted_s(:long_ordinal) # => "November 10th, 2007" # date.to_formatted_s(:rfc822) # => "10 Nov 2007" + # date.to_formatted_s(:iso8601) # => "2007-11-10" # # == Adding your own time formats to to_formatted_s # You can add your own formats to the Date::DATE_FORMATS hash. @@ -75,10 +76,10 @@ class Date # # date.to_time(:utc) # => Sat Nov 10 00:00:00 UTC 2007 def to_time(form = :local) - ::Time.send("#{form}_time", year, month, day) + ::Time.send(form, year, month, day) end def xmlschema - to_time_in_current_zone.xmlschema + in_time_zone.xmlschema end end diff --git a/activesupport/lib/active_support/core_ext/date/zones.rb b/activesupport/lib/active_support/core_ext/date/zones.rb index c1b3934722..d109b430db 100644 --- a/activesupport/lib/active_support/core_ext/date/zones.rb +++ b/activesupport/lib/active_support/core_ext/date/zones.rb @@ -1,15 +1,6 @@ require 'date' -require 'active_support/core_ext/time/zones' +require 'active_support/core_ext/date_and_time/zones' class Date - # Converts Date to a TimeWithZone in the current zone if <tt>Time.zone</tt> or - # <tt>Time.zone_default</tt> is set, otherwise converts Date to a Time via - # Date#to_time. - def to_time_in_current_zone - if ::Time.zone - ::Time.zone.local(year, month, day) - else - to_time - end - end + include DateAndTime::Zones 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 1f78b9eb5a..c869a0e210 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 @@ -78,7 +78,7 @@ module DateAndTime # Returns a new date/time at the start of the month. # DateTime objects will have a time set to 0:00. def beginning_of_month - first_hour{ change(:day => 1) } + first_hour(change(:day => 1)) end alias :at_beginning_of_month :beginning_of_month @@ -93,7 +93,7 @@ module DateAndTime # Returns a new date/time at the end of the quarter. # Example: 31st March, 30th June, 30th September. - # DateTIme objects will have a time set to 23:59:59. + # DateTime objects will have a time set to 23:59:59. def end_of_quarter last_quarter_month = [3, 6, 9, 12].detect { |m| m >= month } beginning_of_month.change(:month => last_quarter_month).end_of_month @@ -109,11 +109,11 @@ module DateAndTime alias :at_beginning_of_year :beginning_of_year # Returns a new date/time representing the given day in the next week. - # Week is assumed to start on +start_day+, default is - # +Date.beginning_of_week+ or +config.beginning_of_week+ when set. - # DateTime objects have their time set to 0:00. - def next_week(start_day = Date.beginning_of_week) - first_hour{ weeks_since(1).beginning_of_week.days_since(days_span(start_day)) } + # The +given_day_in_next_week+ defaults to the beginning of the week + # which is determined by +Date.beginning_of_week+ or +config.beginning_of_week+ + # when set. +DateTime+ objects have their time set to 0:00. + def next_week(given_day_in_next_week = Date.beginning_of_week) + first_hour(weeks_since(1).beginning_of_week.days_since(days_span(given_day_in_next_week))) end # Short-hand for months_since(1). @@ -136,7 +136,7 @@ module DateAndTime # +Date.beginning_of_week+ or +config.beginning_of_week+ when set. # DateTime objects have their time set to 0:00. def prev_week(start_day = Date.beginning_of_week) - first_hour{ weeks_ago(1).beginning_of_week.days_since(days_span(start_day)) } + first_hour(weeks_ago(1).beginning_of_week.days_since(days_span(start_day))) end alias_method :last_week, :prev_week @@ -188,7 +188,7 @@ module DateAndTime # +Date.beginning_of_week+ or +config.beginning_of_week+ when set. # DateTime objects have their time set to 23:59:59. def end_of_week(start_day = Date.beginning_of_week) - last_hour{ days_since(6 - days_to_week_start(start_day)) } + last_hour(days_since(6 - days_to_week_start(start_day))) end alias :at_end_of_week :end_of_week @@ -202,7 +202,7 @@ module DateAndTime # DateTime objects will have a time set to 23:59:59. def end_of_month last_day = ::Time.days_in_month(month, year) - last_hour{ days_since(last_day - day) } + last_hour(days_since(last_day - day)) end alias :at_end_of_month :end_of_month @@ -215,14 +215,12 @@ module DateAndTime private - def first_hour - result = yield - acts_like?(:time) ? result.change(:hour => 0) : result + def first_hour(date_or_time) + date_or_time.acts_like?(:time) ? date_or_time.beginning_of_day : date_or_time end - def last_hour - result = yield - acts_like?(:time) ? result.end_of_day : result + def last_hour(date_or_time) + date_or_time.acts_like?(:time) ? date_or_time.end_of_day : date_or_time end def days_span(day) diff --git a/activesupport/lib/active_support/core_ext/date_and_time/zones.rb b/activesupport/lib/active_support/core_ext/date_and_time/zones.rb new file mode 100644 index 0000000000..96c6df9407 --- /dev/null +++ b/activesupport/lib/active_support/core_ext/date_and_time/zones.rb @@ -0,0 +1,41 @@ +module DateAndTime + module Zones + # Returns the simultaneous time in <tt>Time.zone</tt> if a zone is given or + # if Time.zone_default is set. Otherwise, it returns the current time. + # + # Time.zone = 'Hawaii' # => 'Hawaii' + # DateTime.utc(2000).in_time_zone # => Fri, 31 Dec 1999 14:00:00 HST -10:00 + # Date.new(2000).in_time_zone # => Sat, 01 Jan 2000 00:00:00 HST -10:00 + # + # This method is similar to Time#localtime, except that it uses <tt>Time.zone</tt> as the local zone + # instead of the operating system's time zone. + # + # You can also pass in a TimeZone instance or string that identifies a TimeZone as an argument, + # and the conversion will be based on that zone instead of <tt>Time.zone</tt>. + # + # Time.utc(2000).in_time_zone('Alaska') # => Fri, 31 Dec 1999 15:00:00 AKST -09:00 + # DateTime.utc(2000).in_time_zone('Alaska') # => Fri, 31 Dec 1999 15:00:00 AKST -09:00 + # Date.new(2000).in_time_zone('Alaska') # => Sat, 01 Jan 2000 00:00:00 AKST -09:00 + def in_time_zone(zone = ::Time.zone) + time_zone = ::Time.find_zone! zone + time = acts_like?(:time) ? self : nil + + if time_zone + time_with_zone(time, time_zone) + else + time || self.to_time + end + end + + private + + def time_with_zone(time, zone) + if time + ActiveSupport::TimeWithZone.new(time.utc? ? time : time.getutc, zone) + else + ActiveSupport::TimeWithZone.new(nil, zone, to_time(:utc)) + end + end + end +end + diff --git a/activesupport/lib/active_support/core_ext/date_time/acts_like.rb b/activesupport/lib/active_support/core_ext/date_time/acts_like.rb index c79745c5aa..8fbbe0d3e9 100644 --- a/activesupport/lib/active_support/core_ext/date_time/acts_like.rb +++ b/activesupport/lib/active_support/core_ext/date_time/acts_like.rb @@ -1,3 +1,4 @@ +require 'date' require 'active_support/core_ext/object/acts_like' class DateTime 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 f77d444479..8e5d723074 100644 --- a/activesupport/lib/active_support/core_ext/date_time/calculations.rb +++ b/activesupport/lib/active_support/core_ext/date_time/calculations.rb @@ -1,14 +1,7 @@ -require 'active_support/deprecation' +require 'date' class DateTime class << self - # *DEPRECATED*: Use +DateTime.civil_from_format+ directly. - def local_offset - ActiveSupport::Deprecation.warn 'DateTime.local_offset is deprecated. Use DateTime.civil_from_format directly.' - - ::Time.local(2012).utc_offset.to_r / 86400 - end - # Returns <tt>Time.zone.now.to_datetime</tt> when <tt>Time.zone</tt> or # <tt>config.time_zone</tt> are set, otherwise returns # <tt>Time.now.to_datetime</tt>. @@ -17,16 +10,6 @@ class DateTime end end - # Tells whether the DateTime object's datetime lies in the past. - def past? - self < ::DateTime.current - end - - # Tells whether the DateTime object's datetime lies in the future. - def future? - self > ::DateTime.current - end - # Seconds since midnight: DateTime.now.seconds_since_midnight. def seconds_since_midnight sec + (min * 60) + (hour * 3600) @@ -43,7 +26,7 @@ class DateTime # Returns a new DateTime where one or more of the elements have been changed # according to the +options+ parameter. The time options (<tt>:hour</tt>, - # <tt>:minute</tt>, <tt>:sec</tt>) reset cascadingly, so if only the hour is + # <tt>:min</tt>, <tt>:sec</tt>) reset cascadingly, so if only the hour is # passed, then minute and sec is set to 0. If the hour and minute is passed, # then sec is set to 0. The +options+ parameter takes a hash with any of these # keys: <tt>:year</tt>, <tt>:month</tt>, <tt>:day</tt>, <tt>:hour</tt>, @@ -59,7 +42,7 @@ class DateTime options.fetch(:day, day), options.fetch(:hour, hour), options.fetch(:min, options[:hour] ? 0 : min), - options.fetch(:sec, (options[:hour] || options[:min]) ? 0 : sec), + options.fetch(:sec, (options[:hour] || options[:min]) ? 0 : sec + sec_fraction), options.fetch(:offset, offset), options.fetch(:start, start) ) @@ -106,10 +89,21 @@ class DateTime alias :at_midnight :beginning_of_day alias :at_beginning_of_day :beginning_of_day + # Returns a new DateTime representing the middle of the day (12:00) + def middle_of_day + change(:hour => 12) + end + alias :midday :middle_of_day + alias :noon :middle_of_day + alias :at_midday :middle_of_day + alias :at_noon :middle_of_day + alias :at_middle_of_day :middle_of_day + # Returns a new DateTime representing the end of the day (23:59:59). def end_of_day change(:hour => 23, :min => 59, :sec => 59) end + alias :at_end_of_day :end_of_day # Returns a new DateTime representing the start of the hour (hh:00:00). def beginning_of_hour @@ -121,6 +115,19 @@ class DateTime def end_of_hour change(:min => 59, :sec => 59) end + alias :at_end_of_hour :end_of_hour + + # Returns a new DateTime representing the start of the minute (hh:mm:00). + def beginning_of_minute + change(:sec => 0) + end + alias :at_beginning_of_minute :beginning_of_minute + + # Returns a new DateTime representing the end of the minute (hh:mm:59). + def end_of_minute + change(:sec => 59) + end + alias :at_end_of_minute :end_of_minute # Adjusts DateTime to UTC by adding its offset value; offset is set to 0. # diff --git a/activesupport/lib/active_support/core_ext/date_time/conversions.rb b/activesupport/lib/active_support/core_ext/date_time/conversions.rb index b7d8414a9d..6ddfb72a0d 100644 --- a/activesupport/lib/active_support/core_ext/date_time/conversions.rb +++ b/activesupport/lib/active_support/core_ext/date_time/conversions.rb @@ -1,3 +1,4 @@ +require 'date' require 'active_support/inflector/methods' require 'active_support/core_ext/time/conversions' require 'active_support/core_ext/date_time/calculations' @@ -18,6 +19,7 @@ class DateTime # datetime.to_formatted_s(:long) # => "December 04, 2007 00:00" # datetime.to_formatted_s(:long_ordinal) # => "December 4th, 2007 00:00" # datetime.to_formatted_s(:rfc822) # => "Tue, 04 Dec 2007 00:00:00 +0000" + # datetime.to_formatted_s(:iso8601) # => "2007-12-04T00:00:00+00:00" # # == Adding your own datetime formats to to_formatted_s # DateTime formats are shared with Time. You can add your own to the @@ -79,6 +81,16 @@ class DateTime seconds_since_unix_epoch.to_i end + # Returns the fraction of a second as microseconds + def usec + (sec_fraction * 1_000_000).to_i + end + + # Returns the fraction of a second as nanoseconds + def nsec + (sec_fraction * 1_000_000_000).to_i + end + private def offset_in_seconds diff --git a/activesupport/lib/active_support/core_ext/date_time/zones.rb b/activesupport/lib/active_support/core_ext/date_time/zones.rb index 6457ffbaf6..c39f358395 100644 --- a/activesupport/lib/active_support/core_ext/date_time/zones.rb +++ b/activesupport/lib/active_support/core_ext/date_time/zones.rb @@ -1,24 +1,6 @@ -require 'active_support/core_ext/time/zones' +require 'date' +require 'active_support/core_ext/date_and_time/zones' class DateTime - # Returns the simultaneous time in <tt>Time.zone</tt>. - # - # Time.zone = 'Hawaii' # => 'Hawaii' - # DateTime.new(2000).in_time_zone # => Fri, 31 Dec 1999 14:00:00 HST -10:00 - # - # This method is similar to Time#localtime, except that it uses <tt>Time.zone</tt> - # as the local zone instead of the operating system's time zone. - # - # You can also pass in a TimeZone instance or string that identifies a TimeZone - # as an argument, and the conversion will be based on that zone instead of - # <tt>Time.zone</tt>. - # - # DateTime.new(2000).in_time_zone('Alaska') # => Fri, 31 Dec 1999 15:00:00 AKST -09:00 - def in_time_zone(zone = ::Time.zone) - if zone - ActiveSupport::TimeWithZone.new(utc? ? self : getutc, ::Time.find_zone!(zone)) - else - self - end - end + include DateAndTime::Zones end diff --git a/activesupport/lib/active_support/core_ext/exception.rb b/activesupport/lib/active_support/core_ext/exception.rb deleted file mode 100644 index ba7757ea07..0000000000 --- a/activesupport/lib/active_support/core_ext/exception.rb +++ /dev/null @@ -1,3 +0,0 @@ -module ActiveSupport - FrozenObjectError = RuntimeError -end diff --git a/activesupport/lib/active_support/core_ext/hash.rb b/activesupport/lib/active_support/core_ext/hash.rb index 501483498d..686f12c6da 100644 --- a/activesupport/lib/active_support/core_ext/hash.rb +++ b/activesupport/lib/active_support/core_ext/hash.rb @@ -1,6 +1,5 @@ require 'active_support/core_ext/hash/conversions' require 'active_support/core_ext/hash/deep_merge' -require 'active_support/core_ext/hash/diff' require 'active_support/core_ext/hash/except' require 'active_support/core_ext/hash/indifferent_access' require 'active_support/core_ext/hash/keys' diff --git a/activesupport/lib/active_support/core_ext/hash/conversions.rb b/activesupport/lib/active_support/core_ext/hash/conversions.rb index e1ce9f371a..8930376ac8 100644 --- a/activesupport/lib/active_support/core_ext/hash/conversions.rb +++ b/activesupport/lib/active_support/core_ext/hash/conversions.rb @@ -101,56 +101,57 @@ class Hash # # hash = Hash.from_xml(xml) # # => {"hash"=>{"foo"=>1, "bar"=>2}} - def from_xml(xml) - typecast_xml_value(unrename_keys(ActiveSupport::XmlMini.parse(xml))) + # + # DisallowedType is raise if the XML contains attributes with <tt>type="yaml"</tt> or + # <tt>type="symbol"</tt>. Use <tt>Hash.from_trusted_xml</tt> to parse this XML. + def from_xml(xml, disallowed_types = nil) + ActiveSupport::XMLConverter.new(xml, disallowed_types).to_h + end + + # Builds a Hash from XML just like <tt>Hash.from_xml</tt>, but also allows Symbol and YAML. + def from_trusted_xml(xml) + from_xml xml, [] + end + end +end + +module ActiveSupport + class XMLConverter # :nodoc: + class DisallowedType < StandardError + def initialize(type) + super "Disallowed type attribute: #{type.inspect}" + end + end + + DISALLOWED_TYPES = %w(symbol yaml) + + def initialize(xml, disallowed_types = nil) + @xml = normalize_keys(XmlMini.parse(xml)) + @disallowed_types = disallowed_types || DISALLOWED_TYPES + end + + def to_h + deep_to_h(@xml) end private - def typecast_xml_value(value) - case value + def normalize_keys(params) + case params when Hash - if value['type'] == 'array' - _, entries = Array.wrap(value.detect { |k,v| not v.is_a?(String) }) - if entries.nil? || (c = value['__content__'] && c.blank?) - [] - else - case entries # something weird with classes not matching here. maybe singleton methods breaking is_a? - when Array - entries.collect { |v| typecast_xml_value(v) } - when Hash - [typecast_xml_value(entries)] - else - raise "can't typecast #{entries.inspect}" - end - end - elsif value['type'] == 'file' || - (value['__content__'] && (value.keys.size == 1 || value['__content__'].present?)) - content = value['__content__'] - if parser = ActiveSupport::XmlMini::PARSING[value['type']] - parser.arity == 1 ? parser.call(content) : parser.call(content, value) - else - content - end - elsif value['type'] == 'string' && value['nil'] != 'true' - '' - # blank or nil parsed values are represented by nil - elsif value.blank? || value['nil'] == 'true' - nil - # If the type is the only element which makes it then - # this still makes the value nil, except if type is - # a XML node(where type['value'] is a Hash) - elsif value['type'] && value.size == 1 && !value['type'].is_a?(::Hash) - nil - else - xml_value = Hash[value.map { |k,v| [k, typecast_xml_value(v)] }] + Hash[params.map { |k,v| [k.to_s.tr('-', '_'), normalize_keys(v)] } ] + when Array + params.map { |v| normalize_keys(v) } + else + params + end + end - # Turn { files: { file: #<StringIO> } } into { files: #<StringIO> } so it is compatible with - # how multipart uploaded files from HTML appear - xml_value['file'].is_a?(StringIO) ? xml_value['file'] : xml_value - end + def deep_to_h(value) + case value + when Hash + process_hash(value) when Array - value.map! { |i| typecast_xml_value(i) } - value.length > 1 ? value : value.first + process_array(value) when String value else @@ -158,15 +159,83 @@ class Hash end end - def unrename_keys(params) - case params - when Hash - Hash[params.map { |k,v| [k.to_s.tr('-', '_'), unrename_keys(v)] } ] - when Array - params.map { |v| unrename_keys(v) } + def process_hash(value) + if value.include?('type') && !value['type'].is_a?(Hash) && @disallowed_types.include?(value['type']) + raise DisallowedType, value['type'] + end + + if become_array?(value) + _, entries = Array.wrap(value.detect { |k,v| not v.is_a?(String) }) + if entries.nil? || value['__content__'].try(:empty?) + [] else - params + case entries + when Array + entries.collect { |v| deep_to_h(v) } + when Hash + [deep_to_h(entries)] + else + raise "can't typecast #{entries.inspect}" + end + end + elsif become_content?(value) + process_content(value) + + elsif become_empty_string?(value) + '' + elsif become_hash?(value) + xml_value = Hash[value.map { |k,v| [k, deep_to_h(v)] }] + + # Turn { files: { file: #<StringIO> } } into { files: #<StringIO> } so it is compatible with + # how multipart uploaded files from HTML appear + xml_value['file'].is_a?(StringIO) ? xml_value['file'] : xml_value + end + end + + def become_content?(value) + value['type'] == 'file' || (value['__content__'] && (value.keys.size == 1 || value['__content__'].present?)) + end + + def become_array?(value) + value['type'] == 'array' + end + + def become_empty_string?(value) + # {"string" => true} + # No tests fail when the second term is removed. + value['type'] == 'string' && value['nil'] != 'true' + end + + def become_hash?(value) + !nothing?(value) && !garbage?(value) + end + + def nothing?(value) + # blank or nil parsed values are represented by nil + value.blank? || value['nil'] == 'true' + end + + def garbage?(value) + # If the type is the only element which makes it then + # this still makes the value nil, except if type is + # a XML node(where type['value'] is a Hash) + value['type'] && !value['type'].is_a?(::Hash) && value.size == 1 + end + + def process_content(value) + content = value['__content__'] + if parser = ActiveSupport::XmlMini::PARSING[value['type']] + parser.arity == 1 ? parser.call(content) : parser.call(content, value) + else + content end end + + def process_array(value) + value.map! { |i| deep_to_h(i) } + value.length > 1 ? value : value.first + end + end end + diff --git a/activesupport/lib/active_support/core_ext/hash/diff.rb b/activesupport/lib/active_support/core_ext/hash/diff.rb deleted file mode 100644 index 5f3868b5b0..0000000000 --- a/activesupport/lib/active_support/core_ext/hash/diff.rb +++ /dev/null @@ -1,14 +0,0 @@ -class Hash - # Returns a hash that represents the difference between two hashes. - # - # {1 => 2}.diff(1 => 2) # => {} - # {1 => 2}.diff(1 => 3) # => {1 => 2} - # {}.diff(1 => 2) # => {1 => 2} - # {1 => 2, 3 => 4}.diff(1 => 2) # => {3 => 4} - def diff(other) - ActiveSupport::Deprecation.warn "Hash#diff is no longer used inside of Rails, and is being deprecated with no replacement. If you're using it to compare hashes for the purpose of testing, please use MiniTest's assert_equal instead." - dup. - delete_if { |k, v| other[k] == v }. - merge!(other.dup.delete_if { |k, v| has_key?(k) }) - end -end diff --git a/activesupport/lib/active_support/core_ext/hash/except.rb b/activesupport/lib/active_support/core_ext/hash/except.rb index 5cb00d0ebd..d90e996ad4 100644 --- a/activesupport/lib/active_support/core_ext/hash/except.rb +++ b/activesupport/lib/active_support/core_ext/hash/except.rb @@ -2,7 +2,7 @@ class Hash # Return a hash that includes everything but the given keys. This is useful for # limiting a set of parameters to everything but a few known toggles: # - # @person.update_attributes(params[:person].except(:admin)) + # @person.update(params[:person].except(:admin)) def except(*keys) dup.except!(*keys) end diff --git a/activesupport/lib/active_support/core_ext/hash/slice.rb b/activesupport/lib/active_support/core_ext/hash/slice.rb index 9fa9b3dac4..8ad600b171 100644 --- a/activesupport/lib/active_support/core_ext/hash/slice.rb +++ b/activesupport/lib/active_support/core_ext/hash/slice.rb @@ -26,6 +26,8 @@ class Hash keys.map! { |key| convert_key(key) } if respond_to?(:convert_key, true) omit = slice(*self.keys - keys) hash = slice(*keys) + hash.default = default + hash.default_proc = default_proc if default_proc replace(hash) omit end diff --git a/activesupport/lib/active_support/core_ext/integer/time.rb b/activesupport/lib/active_support/core_ext/integer/time.rb index 9fb4f6b73a..82080ffe51 100644 --- a/activesupport/lib/active_support/core_ext/integer/time.rb +++ b/activesupport/lib/active_support/core_ext/integer/time.rb @@ -1,3 +1,6 @@ +require 'active_support/duration' +require 'active_support/core_ext/numeric/time' + class Integer # Enables the use of time calculations and declarations, like <tt>45.minutes + # 2.hours + 4.years</tt>. diff --git a/activesupport/lib/active_support/core_ext/kernel/reporting.rb b/activesupport/lib/active_support/core_ext/kernel/reporting.rb index 7b518821c8..36ab836457 100644 --- a/activesupport/lib/active_support/core_ext/kernel/reporting.rb +++ b/activesupport/lib/active_support/core_ext/kernel/reporting.rb @@ -59,10 +59,9 @@ module Kernel # # puts 'This code gets executed and nothing related to ZeroDivisionError was seen' def suppress(*exception_classes) - begin yield - rescue Exception => e - raise unless exception_classes.any? { |cls| e.kind_of?(cls) } - end + yield + rescue Exception => e + raise unless exception_classes.any? { |cls| e.kind_of?(cls) } end # Captures the given stream and returns it: @@ -92,6 +91,7 @@ module Kernel stream_io.rewind return captured_stream.read ensure + captured_stream.close captured_stream.unlink stream_io.reopen(origin_stream) end diff --git a/activesupport/lib/active_support/core_ext/logger.rb b/activesupport/lib/active_support/core_ext/logger.rb deleted file mode 100644 index 16fce81445..0000000000 --- a/activesupport/lib/active_support/core_ext/logger.rb +++ /dev/null @@ -1,84 +0,0 @@ -require 'active_support/core_ext/class/attribute_accessors' -require 'active_support/deprecation' - -ActiveSupport::Deprecation.warn 'this file is deprecated and will be removed' - -# Adds the 'around_level' method to Logger. -class Logger #:nodoc: - def self.define_around_helper(level) - module_eval <<-end_eval, __FILE__, __LINE__ + 1 - def around_#{level}(before_message, after_message) # def around_debug(before_message, after_message, &block) - self.#{level}(before_message) # self.debug(before_message) - return_value = yield(self) # return_value = yield(self) - self.#{level}(after_message) # self.debug(after_message) - return_value # return_value - end # end - end_eval - end - [:debug, :info, :error, :fatal].each {|level| define_around_helper(level) } -end - -require 'logger' - -# Extensions to the built-in Ruby logger. -# -# If you want to use the default log formatter as defined in the Ruby core, then you -# will need to set the formatter for the logger as in: -# -# logger.formatter = Formatter.new -# -# You can then specify the datetime format, for example: -# -# logger.datetime_format = "%Y-%m-%d" -# -# Note: This logger is deprecated in favor of ActiveSupport::BufferedLogger -class Logger - ## - # :singleton-method: - # Set to false to disable the silencer - cattr_accessor :silencer - self.silencer = true - - # 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 - yield self - ensure - self.level = old_logger_level - end - else - yield self - end - end - - alias :old_datetime_format= :datetime_format= - # Logging date-time format (string passed to +strftime+). Ignored if the formatter - # does not respond to datetime_format=. - def datetime_format=(format) - formatter.datetime_format = format if formatter.respond_to?(:datetime_format=) - end - - alias :old_datetime_format :datetime_format - # Get the logging datetime format. Returns nil if the formatter does not support - # datetime formatting. - def datetime_format - formatter.datetime_format if formatter.respond_to?(:datetime_format) - end - - alias :old_initialize :initialize - # Overwrite initialize to set a default formatter. - def initialize(*args) - old_initialize(*args) - self.formatter = SimpleFormatter.new - end - - # Simple formatter which only displays the message. - class SimpleFormatter < Logger::Formatter - # This method is invoked when a log event occurs - def call(severity, timestamp, progname, msg) - "#{String === msg ? msg : msg.inspect}\n" - end - end -end diff --git a/activesupport/lib/active_support/core_ext/marshal.rb b/activesupport/lib/active_support/core_ext/marshal.rb index fec3051c0c..56c79c04bd 100644 --- a/activesupport/lib/active_support/core_ext/marshal.rb +++ b/activesupport/lib/active_support/core_ext/marshal.rb @@ -1,21 +1,21 @@ +require 'active_support/core_ext/module/aliasing' + module Marshal class << self def load_with_autoloading(source) - begin - load_without_autoloading(source) - rescue ArgumentError, NameError => exc - if exc.message.match(%r|undefined class/module (.+)|) - # try loading the class/module - $1.constantize - # if it is a IO we need to go back to read the object - source.rewind if source.respond_to?(:rewind) - retry - else - raise exc - end + load_without_autoloading(source) + rescue ArgumentError, NameError => exc + if exc.message.match(%r|undefined class/module (.+)|) + # try loading the class/module + $1.constantize + # if it is a IO we need to go back to read the object + source.rewind if source.respond_to?(:rewind) + retry + else + raise exc end end alias_method_chain :load, :autoloading end -end
\ No newline at end of file +end diff --git a/activesupport/lib/active_support/core_ext/module/delegation.rb b/activesupport/lib/active_support/core_ext/module/delegation.rb index e608eeaf42..182e74d9e1 100644 --- a/activesupport/lib/active_support/core_ext/module/delegation.rb +++ b/activesupport/lib/active_support/core_ext/module/delegation.rb @@ -1,8 +1,19 @@ class Module - # Provides a delegate class method to easily expose contained objects' public methods - # as your own. Pass one or more methods (specified as symbols or strings) - # and the name of the target object via the <tt>:to</tt> option (also a symbol - # or string). At least one method and the <tt>:to</tt> option are required. + # Error generated by +delegate+ when a method is called on +nil+ and +allow_nil+ + # option is not used. + class DelegationError < NoMethodError; end + + # Provides a +delegate+ class method to easily expose contained objects' + # public methods as your own. + # + # ==== Options + # * <tt>:to</tt> - Specifies the target object + # * <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 +NoMethodError+ to be raised + # + # 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 + # (also a symbol or string). # # Delegation is particularly useful with Active Record associations: # @@ -89,29 +100,44 @@ class Module # invoice.customer_name # => 'John Doe' # invoice.customer_address # => 'Vimmersvej 13' # - # If the delegate object is +nil+ an exception is raised, and that happens - # no matter whether +nil+ responds to the delegated method. You can get a - # +nil+ instead with the +:allow_nil+ option. + # If the target is +nil+ and does not respond to the delegated method a + # +NoMethodError+ 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. # - # class Foo - # attr_accessor :bar - # def initialize(bar = nil) - # @bar = bar - # end - # delegate :zoo, to: :bar + # class User < ActiveRecord::Base + # has_one :profile + # delegate :age, to: :profile + # end + # + # User.new.age # raises NoMethodError: undefined method `age' + # + # But if not having a profile yet is fine and should not be an error + # condition: + # + # class User < ActiveRecord::Base + # has_one :profile + # delegate :age, to: :profile, allow_nil: true # end # - # Foo.new.zoo # raises NoMethodError exception (you called nil.zoo) + # User.new.age # nil + # + # Note that if the target is not +nil+ then the call is attempted regardless of the + # <tt>:allow_nil</tt> option, and thus an exception is still raised if said object + # does not respond to the method: # # class Foo - # attr_accessor :bar - # def initialize(bar = nil) + # def initialize(bar) # @bar = bar # end - # delegate :zoo, to: :bar, allow_nil: true + # + # delegate :name, to: :@bar, allow_nil: true # end # - # Foo.new.zoo # returns nil + # Foo.new("Bar").name # raises NoMethodError: undefined method `name' + # def delegate(*methods) options = methods.pop unless options.is_a?(Hash) && to = options[:to] @@ -142,27 +168,36 @@ class Module # methods still accept two arguments. definition = (method =~ /[^\]]=$/) ? 'arg' : '*args, &block' + # The following generated methods call the target exactly once, storing + # the returned value in a dummy variable. + # + # Reason is twofold: On one hand doing less calls is in general better. + # On the other hand it could be that the target has side-effects, + # whereas conceptually, from the user point of view, the delegator should + # be doing one call. if allow_nil - module_eval(<<-EOS, file, line - 2) + module_eval(<<-EOS, file, line - 3) def #{method_prefix}#{method}(#{definition}) # def customer_name(*args, &block) - if #{to} || #{to}.respond_to?(:#{method}) # if client || client.respond_to?(:name) - #{to}.#{method}(#{definition}) # client.name(*args, &block) + _ = #{to} # _ = client + if !_.nil? || nil.respond_to?(:#{method}) # if !_.nil? || nil.respond_to?(:name) + _.#{method}(#{definition}) # _.name(*args, &block) end # end end # end EOS else - exception = %(raise "#{self}##{method_prefix}#{method} delegated to #{to}.#{method}, but #{to} is nil: \#{self.inspect}") + exception = %(raise DelegationError, "#{self}##{method_prefix}#{method} delegated to #{to}.#{method}, but #{to} is nil: \#{self.inspect}") - module_eval(<<-EOS, file, line - 1) - def #{method_prefix}#{method}(#{definition}) # def customer_name(*args, &block) - #{to}.#{method}(#{definition}) # client.name(*args, &block) - rescue NoMethodError # rescue NoMethodError - if #{to}.nil? # if client.nil? - #{exception} # # add helpful message to the exception - else # else - raise # raise - end # end - end # end + module_eval(<<-EOS, file, line - 2) + def #{method_prefix}#{method}(#{definition}) # def customer_name(*args, &block) + _ = #{to} # _ = client + _.#{method}(#{definition}) # _.name(*args, &block) + rescue NoMethodError => e # rescue NoMethodError => e + if _.nil? && e.name == :#{method} # if _.nil? && e.name == :name + #{exception} # # add helpful message to the exception + else # else + raise # raise + end # end + end # end EOS end end diff --git a/activesupport/lib/active_support/core_ext/module/deprecation.rb b/activesupport/lib/active_support/core_ext/module/deprecation.rb index cc45cee5b8..d873de197f 100644 --- a/activesupport/lib/active_support/core_ext/module/deprecation.rb +++ b/activesupport/lib/active_support/core_ext/module/deprecation.rb @@ -14,8 +14,8 @@ class Module # method where you can implement your custom warning behavior. # # class MyLib::Deprecator - # def deprecation_warning(deprecated_method_name, message, caller_backtrace) - # message = "#{method_name} is deprecated and will be removed from MyLibrary | #{message}" + # 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 # 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 08e5f8a5c3..f1d26ef28f 100644 --- a/activesupport/lib/active_support/core_ext/module/introspection.rb +++ b/activesupport/lib/active_support/core_ext/module/introspection.rb @@ -59,20 +59,4 @@ class Module def local_constants #:nodoc: constants(false) end - - # *DEPRECATED*: Use +local_constants+ instead. - # - # Returns the names of the constants defined locally as strings. - # - # module M - # X = 1 - # end - # M.local_constant_names # => ["X"] - # - # This method is useful for forward compatibility, since Ruby 1.8 returns - # constant names as strings, whereas 1.9 returns them as symbols. - def local_constant_names - ActiveSupport::Deprecation.warn 'Module#local_constant_names is deprecated, use Module#local_constants instead' - local_constants.map { |c| c.to_s } - end end diff --git a/activesupport/lib/active_support/core_ext/module/method_transplanting.rb b/activesupport/lib/active_support/core_ext/module/method_transplanting.rb new file mode 100644 index 0000000000..b1097cc83b --- /dev/null +++ b/activesupport/lib/active_support/core_ext/module/method_transplanting.rb @@ -0,0 +1,11 @@ +class Module + ### + # TODO: remove this after 1.9 support is dropped + def methods_transplantable? # :nodoc: + x = Module.new { def foo; end } + Module.new { define_method :bar, x.instance_method(:foo) } + true + rescue TypeError + false + end +end diff --git a/activesupport/lib/active_support/core_ext/object/acts_like.rb b/activesupport/lib/active_support/core_ext/object/acts_like.rb index fcc8e50f06..3912cc5ace 100644 --- a/activesupport/lib/active_support/core_ext/object/acts_like.rb +++ b/activesupport/lib/active_support/core_ext/object/acts_like.rb @@ -1,9 +1,9 @@ class Object # A duck-type assistant method. For example, Active Support extends Date - # to define an acts_like_date? method, and extends Time to define - # acts_like_time?. As a result, we can do "x.acts_like?(:time)" and - # "x.acts_like?(:date)" to do duck-type-safe comparisons, since classes that - # we want to act like Time simply need to define an acts_like_time? method. + # to define an <tt>acts_like_date?</tt> method, and extends Time to define + # <tt>acts_like_time?</tt>. As a result, we can do <tt>x.acts_like?(:time)</tt> and + # <tt>x.acts_like?(:date)</tt> to do duck-type-safe comparisons, since classes that + # we want to act like Time simply need to define an <tt>acts_like_time?</tt> method. def acts_like?(duck) respond_to? :"acts_like_#{duck}?" end diff --git a/activesupport/lib/active_support/core_ext/object/inclusion.rb b/activesupport/lib/active_support/core_ext/object/inclusion.rb index 3fec465ec0..b5671f66d0 100644 --- a/activesupport/lib/active_support/core_ext/object/inclusion.rb +++ b/activesupport/lib/active_support/core_ext/object/inclusion.rb @@ -1,25 +1,15 @@ class Object - # Returns true if this object is included in the argument(s). Argument must be - # any object which responds to +#include?+ or optionally, multiple arguments can be passed in. Usage: + # Returns true if this object is included in the argument. Argument must be + # any object which responds to +#include?+. Usage: # - # characters = ['Konata', 'Kagami', 'Tsukasa'] - # 'Konata'.in?(characters) # => true + # characters = ["Konata", "Kagami", "Tsukasa"] + # "Konata".in?(characters) # => true # - # character = 'Konata' - # character.in?('Konata', 'Kagami', 'Tsukasa') # => true - # - # This will throw an ArgumentError if a single argument is passed in and it doesn't respond + # This will throw an ArgumentError if the argument doesn't respond # to +#include?+. - def in?(*args) - if args.length > 1 - args.include? self - else - another_object = args.first - if another_object.respond_to? :include? - another_object.include? self - else - raise ArgumentError.new 'The single parameter passed to #in? must respond to #include?' - end - end + def in?(another_object) + another_object.include?(self) + rescue NoMethodError + raise ArgumentError.new("The parameter passed to #in? must respond to #include?") end end diff --git a/activesupport/lib/active_support/core_ext/object/instance_variables.rb b/activesupport/lib/active_support/core_ext/object/instance_variables.rb index 40821fd619..755e1c6b16 100644 --- a/activesupport/lib/active_support/core_ext/object/instance_variables.rb +++ b/activesupport/lib/active_support/core_ext/object/instance_variables.rb @@ -13,7 +13,7 @@ class Object Hash[instance_variables.map { |name| [name[1..-1], instance_variable_get(name)] }] end - # Returns an array of instance variable names including "@". + # Returns an array of instance variable names as strings including "@". # # class C # def initialize(x, y) diff --git a/activesupport/lib/active_support/core_ext/object/to_param.rb b/activesupport/lib/active_support/core_ext/object/to_param.rb index 0d5f3501e5..3b137ce6ae 100644 --- a/activesupport/lib/active_support/core_ext/object/to_param.rb +++ b/activesupport/lib/active_support/core_ext/object/to_param.rb @@ -53,6 +53,6 @@ class Hash def to_param(namespace = nil) collect do |key, value| value.to_query(namespace ? "#{namespace}[#{key}]" : key) - end.sort * '&' + end.sort! * '&' end end diff --git a/activesupport/lib/active_support/core_ext/object/try.rb b/activesupport/lib/active_support/core_ext/object/try.rb index 1079ddde98..48190e1e66 100644 --- a/activesupport/lib/active_support/core_ext/object/try.rb +++ b/activesupport/lib/active_support/core_ext/object/try.rb @@ -1,35 +1,43 @@ class Object - # Invokes the public method identified by the symbol +method+, passing it any arguments - # and/or the block specified, just like the regular Ruby <tt>Object#public_send</tt> does. + # Invokes the public method whose name goes as first argument just like + # +public_send+ does, except that if the receiver does not respond to it the + # call returns +nil+ rather than raising an exception. # - # *Unlike* that method however, a +NoMethodError+ exception will *not* be raised - # and +nil+ will be returned instead, if the receiving object is a +nil+ object or NilClass. + # This method is defined to be able to write # - # This is also true if the receiving object does not implemented the tried method. It will - # return +nil+ in that case as well. - # - # If try is called without a method to call, it will yield any given block with the object. + # @person.try(:name) # - # Please also note that +try+ is defined on +Object+, therefore it won't work with - # subclasses of +BasicObject+. For example, using try with +SimpleDelegator+ will - # delegate +try+ to target instead of calling it on delegator itself. + # instead of # - # Without +try+ - # @person && @person.name - # or # @person ? @person.name : nil # - # With +try+ - # @person.try(:name) + # +try+ returns +nil+ when called on +nil+ regardless of whether it responds + # to the method: + # + # nil.try(:to_i) # => nil, rather than 0 + # + # Arguments and blocks are forwarded to the method if invoked: + # + # @posts.try(:each_slice, 2) do |a, b| + # ... + # end + # + # The number of arguments in the signature must match. If the object responds + # to the method the call is attempted and +ArgumentError+ is still raised + # otherwise. # - # +try+ also accepts arguments and/or a block, for the method it is trying - # Person.try(:find, 1) - # @people.try(:collect) {|p| p.name} + # If +try+ is called without arguments it yields the receiver to a given + # block unless it is +nil+: # - # Without a method argument try will yield to the block unless the receiver is nil. - # @person.try { |p| "#{p.first_name} #{p.last_name}" } + # @person.try do |p| + # ... + # end # - # +try+ behaves like +Object#public_send+, unless called on +NilClass+. + # Please also note that +try+ is defined on +Object+, therefore it won't work + # with instances of classes that do not have +Object+ among their ancestors, + # like direct subclasses of +BasicObject+. For example, using +try+ with + # +SimpleDelegator+ will delegate +try+ to the target instead of calling it on + # delegator itself. def try(*a, &b) if a.empty? && block_given? yield self @@ -39,7 +47,7 @@ class Object end # Same as #try, but will raise a NoMethodError exception if the receiving is not nil and - # does not implemented the tried method. + # does not implement the tried method. def try!(*a, &b) if a.empty? && block_given? yield self diff --git a/activesupport/lib/active_support/core_ext/proc.rb b/activesupport/lib/active_support/core_ext/proc.rb deleted file mode 100644 index 166c3855a0..0000000000 --- a/activesupport/lib/active_support/core_ext/proc.rb +++ /dev/null @@ -1,17 +0,0 @@ -require "active_support/core_ext/kernel/singleton_class" -require "active_support/deprecation" - -class Proc #:nodoc: - def bind(object) - ActiveSupport::Deprecation.warn 'Proc#bind is deprecated and will be removed in future versions' - - block, time = self, Time.now - object.class_eval do - method_name = "__bind_#{time.to_i}_#{time.usec}" - define_method(method_name, &block) - method = instance_method(method_name) - remove_method(method_name) - method - end.bind(object) - end -end diff --git a/activesupport/lib/active_support/core_ext/range.rb b/activesupport/lib/active_support/core_ext/range.rb index 1d8b1ede5a..9368e81235 100644 --- a/activesupport/lib/active_support/core_ext/range.rb +++ b/activesupport/lib/active_support/core_ext/range.rb @@ -1,3 +1,4 @@ require 'active_support/core_ext/range/conversions' require 'active_support/core_ext/range/include_range' require 'active_support/core_ext/range/overlaps' +require 'active_support/core_ext/range/each' diff --git a/activesupport/lib/active_support/core_ext/range/each.rb b/activesupport/lib/active_support/core_ext/range/each.rb new file mode 100644 index 0000000000..d51ea2e944 --- /dev/null +++ b/activesupport/lib/active_support/core_ext/range/each.rb @@ -0,0 +1,24 @@ +require 'active_support/core_ext/module/aliasing' +require 'active_support/core_ext/object/acts_like' + +class Range #:nodoc: + + def each_with_time_with_zone(&block) + ensure_iteration_allowed + each_without_time_with_zone(&block) + end + alias_method_chain :each, :time_with_zone + + def step_with_time_with_zone(n = 1, &block) + ensure_iteration_allowed + step_without_time_with_zone(n, &block) + end + alias_method_chain :step, :time_with_zone + + private + def ensure_iteration_allowed + if first.acts_like?(:time) + raise TypeError, "can't iterate from #{first.class}" + end + end +end 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 3af66aaf2f..3a07401c8a 100644 --- a/activesupport/lib/active_support/core_ext/range/include_range.rb +++ b/activesupport/lib/active_support/core_ext/range/include_range.rb @@ -1,3 +1,5 @@ +require 'active_support/core_ext/module/aliasing' + class Range # Extends the default Range#include? to support range comparisons. # (1..5).include?(1..5) # => true diff --git a/activesupport/lib/active_support/core_ext/string.rb b/activesupport/lib/active_support/core_ext/string.rb index ad864765a3..c656db2c6c 100644 --- a/activesupport/lib/active_support/core_ext/string.rb +++ b/activesupport/lib/active_support/core_ext/string.rb @@ -4,10 +4,10 @@ require 'active_support/core_ext/string/multibyte' require 'active_support/core_ext/string/starts_ends_with' require 'active_support/core_ext/string/inflections' require 'active_support/core_ext/string/access' -require 'active_support/core_ext/string/xchar' require 'active_support/core_ext/string/behavior' require 'active_support/core_ext/string/output_safety' require 'active_support/core_ext/string/exclude' require 'active_support/core_ext/string/strip' require 'active_support/core_ext/string/inquiry' require 'active_support/core_ext/string/indent' +require 'active_support/core_ext/string/zones' diff --git a/activesupport/lib/active_support/core_ext/string/access.rb b/activesupport/lib/active_support/core_ext/string/access.rb index 8fa8157d65..ee3b6d2b3f 100644 --- a/activesupport/lib/active_support/core_ext/string/access.rb +++ b/activesupport/lib/active_support/core_ext/string/access.rb @@ -59,7 +59,7 @@ class String # str.from(0).to(-1) #=> "hello" # str.from(1).to(-2) #=> "ell" def to(position) - self[0..position] + self[0, position + 1] end # Returns the first character. If a limit is supplied, returns a substring diff --git a/activesupport/lib/active_support/core_ext/string/conversions.rb b/activesupport/lib/active_support/core_ext/string/conversions.rb index 9b9d83932e..6691fc0995 100644 --- a/activesupport/lib/active_support/core_ext/string/conversions.rb +++ b/activesupport/lib/active_support/core_ext/string/conversions.rb @@ -3,26 +3,35 @@ require 'active_support/core_ext/time/calculations' class String # Converts a string to a Time value. - # The +form+ can be either :utc or :local (default :utc). + # The +form+ can be either :utc or :local (default :local). # - # The time is parsed using Date._parse method. - # If +form+ is :local, then time is formatted using Time.zone + # The time is parsed using Time.parse method. + # If +form+ is :local, then the time is in the system timezone. + # If the date part is missing then the current date is used and if + # the time part is missing then it is assumed to be 00:00:00. # - # "3-2-2012".to_time # => 2012-02-03 00:00:00 UTC - # "12:20".to_time # => ArgumentError: invalid date - # "2012-12-13 06:12".to_time # => 2012-12-13 06:12:00 UTC - # "2012-12-13T06:12".to_time # => 2012-12-13 06:12:00 UTC - # "2012-12-13T06:12".to_time(:local) # => 2012-12-13 06:12:00 +0100 - def to_time(form = :utc) - unless blank? - date_values = ::Date._parse(self, false). - values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction, :offset). - map! { |arg| arg || 0 } - date_values[6] *= 1000000 - offset = date_values.pop + # "13-12-2012".to_time # => 2012-12-13 00:00:00 +0100 + # "06:12".to_time # => 2012-12-13 06:12:00 +0100 + # "2012-12-13 06:12".to_time # => 2012-12-13 06:12:00 +0100 + # "2012-12-13T06:12".to_time # => 2012-12-13 06:12:00 +0100 + # "2012-12-13T06:12".to_time(:utc) # => 2012-12-13 05:12:00 UTC + # "12/13/2012".to_time # => ArgumentError: argument out of range + def to_time(form = :local) + parts = Date._parse(self, false) + return if parts.empty? - ::Time.send("#{form}_time", *date_values) - offset - end + now = Time.now + time = Time.new( + parts.fetch(:year, now.year), + parts.fetch(:mon, now.month), + parts.fetch(:mday, now.day), + parts.fetch(:hour, 0), + parts.fetch(:min, 0), + parts.fetch(:sec, 0) + parts.fetch(:sec_fraction, 0), + parts.fetch(:offset, form == :utc ? 0 : nil) + ) + + form == :utc ? time.utc : time.getlocal end # Converts a string to a Date value. @@ -32,11 +41,7 @@ class String # "2012-12-13".to_date #=> Thu, 13 Dec 2012 # "12/13/2012".to_date #=> ArgumentError: invalid date def to_date - unless blank? - date_values = ::Date._parse(self, false).values_at(:year, :mon, :mday) - - ::Date.new(*date_values) - end + ::Date.parse(self, false) unless blank? end # Converts a string to a DateTime value. @@ -46,13 +51,6 @@ class String # "2012-12-13 12:50".to_datetime #=> Thu, 13 Dec 2012 12:50:00 +0000 # "12/13/2012".to_datetime #=> ArgumentError: invalid date def to_datetime - unless blank? - date_values = ::Date._parse(self, false). - values_at(:year, :mon, :mday, :hour, :min, :sec, :zone, :sec_fraction). - map! { |arg| arg || 0 } - date_values[5] += date_values.pop - - ::DateTime.civil(*date_values) - end + ::DateTime.parse(self, false) unless blank? end end diff --git a/activesupport/lib/active_support/core_ext/string/encoding.rb b/activesupport/lib/active_support/core_ext/string/encoding.rb deleted file mode 100644 index a583b914db..0000000000 --- a/activesupport/lib/active_support/core_ext/string/encoding.rb +++ /dev/null @@ -1,8 +0,0 @@ -require 'active_support/deprecation' - -class String - def encoding_aware? - ActiveSupport::Deprecation.warn 'String#encoding_aware? is deprecated' - true - end -end diff --git a/activesupport/lib/active_support/core_ext/string/filters.rb b/activesupport/lib/active_support/core_ext/string/filters.rb index e05447439a..49c0df6026 100644 --- a/activesupport/lib/active_support/core_ext/string/filters.rb +++ b/activesupport/lib/active_support/core_ext/string/filters.rb @@ -3,6 +3,8 @@ class String # the string, and then changing remaining consecutive whitespace # groups into one space each. # + # Note that it handles both ASCII and Unicode whitespace like mongolian vowel separator (U+180E). + # # %{ Multi-line # string }.squish # => "Multi-line string" # " foo bar \n \t boo".squish # => "foo bar boo" @@ -12,11 +14,22 @@ class String # Performs a destructive squish. See String#squish. def squish! - strip! - gsub!(/\s+/, ' ') + gsub!(/\A[[:space:]]+/, '') + gsub!(/[[:space:]]+\z/, '') + gsub!(/[[:space:]]+/, ' ') self end + # Returns a new string with all occurrences of the pattern removed. Short-hand for String#gsub(pattern, ''). + def remove(pattern) + gsub pattern, '' + end + + # Alters the string by removing all occurrences of the pattern. Short-hand for String#gsub!(pattern, ''). + def remove!(pattern) + gsub! pattern, '' + end + # Truncates a given +text+ after a given <tt>length</tt> if +text+ is longer than <tt>length</tt>: # # 'Once upon a time in a world far far away'.truncate(27) @@ -38,8 +51,8 @@ class String def truncate(truncate_at, options = {}) return dup unless length > truncate_at - options[:omission] ||= '...' - length_with_room_for_omission = truncate_at - options[:omission].length + omission = options[:omission] || '...' + length_with_room_for_omission = truncate_at - omission.length stop = \ if options[:separator] rindex(options[:separator], length_with_room_for_omission) || length_with_room_for_omission @@ -47,6 +60,6 @@ class String length_with_room_for_omission end - self[0...stop] + options[:omission] + "#{self[0, stop]}#{omission}" end end diff --git a/activesupport/lib/active_support/core_ext/string/indent.rb b/activesupport/lib/active_support/core_ext/string/indent.rb index afc3032272..ce3a69cf5f 100644 --- a/activesupport/lib/active_support/core_ext/string/indent.rb +++ b/activesupport/lib/active_support/core_ext/string/indent.rb @@ -29,7 +29,7 @@ class String # "foo\n\t\tbar".indent(2) # => "\t\tfoo\n\t\t\t\tbar" # "foo".indent(2, "\t") # => "\t\tfoo" # - # While +indent_string+ is tipically one space or tab, it may be any string. + # While +indent_string+ is typically one space or tab, it may be any string. # # The third argument, +indent_empty_lines+, is a flag that says whether # empty lines should be indented. Default is false. diff --git a/activesupport/lib/active_support/core_ext/string/inflections.rb b/activesupport/lib/active_support/core_ext/string/inflections.rb index 341e2deec9..56e8a5f98d 100644 --- a/activesupport/lib/active_support/core_ext/string/inflections.rb +++ b/activesupport/lib/active_support/core_ext/string/inflections.rb @@ -41,7 +41,7 @@ class String # # If the optional parameter +locale+ is specified, # the word will be singularized as a word of that language. - # By default, this paramter is set to <tt>:en</tt>. + # By default, this parameter is set to <tt>:en</tt>. # You must define your own inflection rules for languages other than English. # # 'posts'.singularize # => "post" @@ -185,7 +185,7 @@ class String # # Singular names are not handled correctly. # - # 'business'.classify # => "Busines" + # 'business'.classify # => "Business" def classify ActiveSupport::Inflector.classify(self) end @@ -193,8 +193,8 @@ class String # Capitalizes the first word, turns underscores into spaces, and strips '_id'. # Like +titleize+, this is meant for creating pretty output. # - # 'employee_salary' # => "Employee salary" - # 'author_id' # => "Author" + # 'employee_salary'.humanize # => "Employee salary" + # 'author_id'.humanize # => "Author" def humanize ActiveSupport::Inflector.humanize(self) 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 5f85cedcf5..dc033ed11b 100644 --- a/activesupport/lib/active_support/core_ext/string/output_safety.rb +++ b/activesupport/lib/active_support/core_ext/string/output_safety.rb @@ -42,7 +42,7 @@ class ERB # html_escape_once('<< Accept & Checkout') # # => "<< Accept & Checkout" def html_escape_once(s) - result = s.to_s.gsub(HTML_ESCAPE_ONCE_REGEXP) { |special| HTML_ESCAPE[special] } + result = s.to_s.gsub(HTML_ESCAPE_ONCE_REGEXP, HTML_ESCAPE) s.html_safe? ? result.html_safe : result end @@ -60,7 +60,7 @@ class ERB # json_escape('{"name":"john","created_at":"2010-04-28T01:39:31Z","id":1}') # # => {name:john,created_at:2010-04-28T01:39:31Z,id:1} def json_escape(s) - result = s.to_s.gsub(JSON_ESCAPE_REGEXP) { |special| JSON_ESCAPE[special] } + result = s.to_s.gsub(JSON_ESCAPE_REGEXP, JSON_ESCAPE) s.html_safe? ? result.html_safe : result end diff --git a/activesupport/lib/active_support/core_ext/string/xchar.rb b/activesupport/lib/active_support/core_ext/string/xchar.rb deleted file mode 100644 index f9a5b4fb64..0000000000 --- a/activesupport/lib/active_support/core_ext/string/xchar.rb +++ /dev/null @@ -1,18 +0,0 @@ -begin - # See http://fast-xs.rubyforge.org/ by Eric Wong. - # Also included with hpricot. - require 'fast_xs' -rescue LoadError - # fast_xs extension unavailable -else - begin - require 'builder' - rescue LoadError - # builder demands the first shot at defining String#to_xs - end - - class String - alias_method :original_xs, :to_xs if method_defined?(:to_xs) - alias_method :to_xs, :fast_xs - end -end diff --git a/activesupport/lib/active_support/core_ext/string/zones.rb b/activesupport/lib/active_support/core_ext/string/zones.rb new file mode 100644 index 0000000000..510c884c18 --- /dev/null +++ b/activesupport/lib/active_support/core_ext/string/zones.rb @@ -0,0 +1,14 @@ +require 'active_support/core_ext/string/conversions' +require 'active_support/core_ext/time/zones' + +class String + # Converts String to a TimeWithZone in the current zone if Time.zone or Time.zone_default + # is set, otherwise converts String to a Time via String#to_time + def in_time_zone(zone = ::Time.zone) + if zone + ::Time.find_zone!(zone).parse(self) + else + to_time + end + end +end diff --git a/activesupport/lib/active_support/core_ext/thread.rb b/activesupport/lib/active_support/core_ext/thread.rb new file mode 100644 index 0000000000..e80f442973 --- /dev/null +++ b/activesupport/lib/active_support/core_ext/thread.rb @@ -0,0 +1,79 @@ +class Thread + LOCK = Mutex.new # :nodoc: + + # Returns the value of a thread local variable that has been set. Note that + # these are different than fiber local values. + # + # Thread local values are carried along with threads, and do not respect + # fibers. For example: + # + # Thread.new { + # Thread.current.thread_variable_set("foo", "bar") # set a thread local + # Thread.current["foo"] = "bar" # set a fiber local + # + # Fiber.new { + # Fiber.yield [ + # Thread.current.thread_variable_get("foo"), # get the thread local + # Thread.current["foo"], # get the fiber local + # ] + # }.resume + # }.join.value # => ['bar', nil] + # + # The value <tt>"bar"</tt> is returned for the thread local, where +nil+ is returned + # for the fiber local. The fiber is executed in the same thread, so the + # thread local values are available. + def thread_variable_get(key) + _locals[key.to_sym] + end + + # Sets a thread local with +key+ to +value+. Note that these are local to + # threads, and not to fibers. Please see Thread#thread_variable_get for + # more information. + def thread_variable_set(key, value) + _locals[key.to_sym] = value + end + + # Returns an an array of the names of the thread-local variables (as Symbols). + # + # thr = Thread.new do + # Thread.current.thread_variable_set(:cat, 'meow') + # Thread.current.thread_variable_set("dog", 'woof') + # end + # thr.join #=> #<Thread:0x401b3f10 dead> + # thr.thread_variables #=> [:dog, :cat] + # + # Note that these are not fiber local variables. Please see Thread#thread_variable_get + # for more details. + def thread_variables + _locals.keys + end + + # Returns <tt>true</tt> if the given string (or symbol) exists as a + # thread-local variable. + # + # me = Thread.current + # me.thread_variable_set(:oliver, "a") + # me.thread_variable?(:oliver) #=> true + # me.thread_variable?(:stanley) #=> false + # + # Note that these are not fiber local variables. Please see Thread#thread_variable_get + # for more details. + def thread_variable?(key) + _locals.has_key?(key.to_sym) + end + + def freeze + _locals.freeze + super + end + + private + + def _locals + if defined?(@_locals) + @_locals + else + LOCK.synchronize { @_locals ||= {} } + end + end +end unless Thread.instance_methods.include?(:thread_variable_set) diff --git a/activesupport/lib/active_support/core_ext/time/calculations.rb b/activesupport/lib/active_support/core_ext/time/calculations.rb index 46c9f05c15..6e0af0db4d 100644 --- a/activesupport/lib/active_support/core_ext/time/calculations.rb +++ b/activesupport/lib/active_support/core_ext/time/calculations.rb @@ -25,36 +25,27 @@ class Time end end - # Returns a new Time if requested year can be accommodated by Ruby's Time class - # (i.e., if year is within either 1970..2038 or 1902..2038, depending on system architecture); - # otherwise returns a DateTime. - def time_with_datetime_fallback(utc_or_local, year, month=1, day=1, hour=0, min=0, sec=0, usec=0) - time = ::Time.send(utc_or_local, year, month, day, hour, min, sec, usec) - - # This check is needed because Time.utc(y) returns a time object in the 2000s for 0 <= y <= 138. - if time.year == year - time - else - ::DateTime.civil_from_format(utc_or_local, year, month, day, hour, min, sec) - end - rescue - ::DateTime.civil_from_format(utc_or_local, year, month, day, hour, min, sec) + # Returns <tt>Time.zone.now</tt> when <tt>Time.zone</tt> or <tt>config.time_zone</tt> are set, otherwise just returns <tt>Time.now</tt>. + def current + ::Time.zone ? ::Time.zone.now : ::Time.now end - # Wraps class method +time_with_datetime_fallback+ with +utc_or_local+ set to <tt>:utc</tt>. - def utc_time(*args) - time_with_datetime_fallback(:utc, *args) - end + # Layers additional behavior on Time.at so that ActiveSupport::TimeWithZone and DateTime + # instances can be used when called with a single argument + def at_with_coercion(*args) + return at_without_coercion(*args) if args.size != 1 - # Wraps class method +time_with_datetime_fallback+ with +utc_or_local+ set to <tt>:local</tt>. - def local_time(*args) - time_with_datetime_fallback(:local, *args) - end + # Time.at can be called with a time or numerical value + time_or_number = args.first - # Returns <tt>Time.zone.now</tt> when <tt>Time.zone</tt> or <tt>config.time_zone</tt> are set, otherwise just returns <tt>Time.now</tt>. - def current - ::Time.zone ? ::Time.zone.now : ::Time.now + if time_or_number.is_a?(ActiveSupport::TimeWithZone) || time_or_number.is_a?(DateTime) + at_without_coercion(time_or_number.to_f).getlocal + else + at_without_coercion(time_or_number) + end end + alias_method :at_without_coercion, :at + alias_method :at, :at_with_coercion end # Seconds since midnight: Time.now.seconds_since_midnight @@ -151,6 +142,16 @@ class Time alias :at_midnight :beginning_of_day alias :at_beginning_of_day :beginning_of_day + # Returns a new Time representing the middle of the day (12:00) + def middle_of_day + change(:hour => 12) + end + alias :midday :middle_of_day + alias :noon :middle_of_day + alias :at_midday :middle_of_day + alias :at_noon :middle_of_day + alias :at_middle_of_day :middle_of_day + # Returns a new Time representing the end of the day, 23:59:59.999999 (.999999999 in ruby1.9) def end_of_day change( @@ -160,6 +161,7 @@ class Time :usec => Rational(999999999, 1000) ) end + alias :at_end_of_day :end_of_day # Returns a new Time representing the start of the hour (x:00) def beginning_of_hour @@ -175,6 +177,22 @@ class Time :usec => Rational(999999999, 1000) ) end + alias :at_end_of_hour :end_of_hour + + # Returns a new Time representing the start of the minute (x:xx:00) + def beginning_of_minute + change(:sec => 0) + end + alias :at_beginning_of_minute :beginning_of_minute + + # Returns a new Time representing the end of the minute, x:xx:59.999999 (.999999999 in ruby1.9) + def end_of_minute + change( + :sec => 59, + :usec => Rational(999999999, 1000) + ) + end + alias :at_end_of_minute :end_of_minute # Returns a Range representing the whole day of the current time. def all_day diff --git a/activesupport/lib/active_support/core_ext/time/conversions.rb b/activesupport/lib/active_support/core_ext/time/conversions.rb index 48654eb1cc..9fd26156c7 100644 --- a/activesupport/lib/active_support/core_ext/time/conversions.rb +++ b/activesupport/lib/active_support/core_ext/time/conversions.rb @@ -16,7 +16,8 @@ class Time :rfc822 => lambda { |time| offset_format = time.formatted_offset(false) time.strftime("%a, %d %b %Y %H:%M:%S #{offset_format}") - } + }, + :iso8601 => lambda { |time| time.iso8601 } } # Converts to a formatted string. See DATE_FORMATS for builtin formats. @@ -34,6 +35,7 @@ class Time # time.to_formatted_s(:long) # => "January 18, 2007 06:10" # time.to_formatted_s(:long_ordinal) # => "January 18th, 2007 06:10" # time.to_formatted_s(:rfc822) # => "Thu, 18 Jan 2007 06:10:17 -0600" + # time.to_formatted_s(:iso8601) # => "2007-01-18T06:10:17-06:00" # # == Adding your own time formats to +to_formatted_s+ # You can add your own formats to the Time::DATE_FORMATS hash. diff --git a/activesupport/lib/active_support/core_ext/time/zones.rb b/activesupport/lib/active_support/core_ext/time/zones.rb index 796c5f9805..bbda04d60c 100644 --- a/activesupport/lib/active_support/core_ext/time/zones.rb +++ b/activesupport/lib/active_support/core_ext/time/zones.rb @@ -1,8 +1,8 @@ require 'active_support/time_with_zone' +require 'active_support/core_ext/date_and_time/zones' class Time - @zone_default = nil - + include DateAndTime::Zones class << self attr_accessor :zone_default @@ -75,24 +75,4 @@ class Time find_zone!(time_zone) rescue nil end end - - # Returns the simultaneous time in <tt>Time.zone</tt>. - # - # Time.zone = 'Hawaii' # => 'Hawaii' - # Time.utc(2000).in_time_zone # => Fri, 31 Dec 1999 14:00:00 HST -10:00 - # - # This method is similar to Time#localtime, except that it uses <tt>Time.zone</tt> as the local zone - # instead of the operating system's time zone. - # - # You can also pass in a TimeZone instance or string that identifies a TimeZone as an argument, - # and the conversion will be based on that zone instead of <tt>Time.zone</tt>. - # - # Time.utc(2000).in_time_zone('Alaska') # => Fri, 31 Dec 1999 15:00:00 AKST -09:00 - def in_time_zone(zone = ::Time.zone) - if zone - ActiveSupport::TimeWithZone.new(utc? ? self : getutc, ::Time.find_zone!(zone)) - else - self - end - end end diff --git a/activesupport/lib/active_support/dependencies.rb b/activesupport/lib/active_support/dependencies.rb index efd351d741..19d4ff51d7 100644 --- a/activesupport/lib/active_support/dependencies.rb +++ b/activesupport/lib/active_support/dependencies.rb @@ -1,5 +1,6 @@ require 'set' require 'thread' +require 'thread_safe' require 'pathname' require 'active_support/core_ext/module/aliasing' require 'active_support/core_ext/module/attribute_accessors' @@ -7,6 +8,7 @@ require 'active_support/core_ext/module/introspection' require 'active_support/core_ext/module/anonymous' require 'active_support/core_ext/module/qualified_const' require 'active_support/core_ext/object/blank' +require 'active_support/core_ext/kernel/reporting' require 'active_support/core_ext/load_error' require 'active_support/core_ext/name_error' require 'active_support/core_ext/string/starts_ends_with' @@ -197,9 +199,19 @@ module ActiveSupport #:nodoc: Dependencies.require_or_load(file_name) end + # 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>. + # + # Use this method in code that absolutely needs a certain constant to be + # defined at that point. A typical use case is to make constant name + # resolution deterministic for constants with the same relative name in + # different namespaces whose evaluation would depend on load order + # otherwise. def require_dependency(file_name, message = "No such file to load -- %s") + file_name = file_name.to_path if file_name.respond_to?(:to_path) unless file_name.is_a?(String) - raise ArgumentError, "the file name must be a String -- you passed #{file_name.inspect}" + raise ArgumentError, "the file name must either be a String or implement #to_path -- you passed #{file_name.inspect}" end Dependencies.depend_on(file_name, message) @@ -212,7 +224,7 @@ module ActiveSupport #:nodoc: yield end rescue Exception => exception # errors from loading file - exception.blame_file! file + exception.blame_file! file if exception.respond_to? :blame_file! raise end @@ -415,7 +427,7 @@ module ActiveSupport #:nodoc: def load_file(path, const_paths = loadable_constants_for_path(path)) log_call path, const_paths const_paths = [const_paths].compact unless const_paths.is_a? Array - parent_paths = const_paths.collect { |const_path| const_path[/.*(?=::)/] || :Object } + parent_paths = const_paths.collect { |const_path| const_path[/.*(?=::)/] || ::Object } result = nil newly_defined_paths = new_constants_in(*parent_paths) do @@ -458,7 +470,7 @@ module ActiveSupport #:nodoc: if loaded.include?(expanded) raise "Circular dependency detected while autoloading constant #{qualified_name}" else - require_or_load(expanded) + 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) end @@ -517,7 +529,7 @@ module ActiveSupport #:nodoc: class ClassCache def initialize - @store = Hash.new + @store = ThreadSafe::Cache.new end def empty? @@ -633,7 +645,7 @@ module ActiveSupport #:nodoc: when String then desc.sub(/^::/, '') when Symbol then desc.to_s when Module - desc.name.presence || + desc.name || raise(ArgumentError, "Anonymous modules have no name to be referenced by") else raise TypeError, "Not a valid constant descriptor: #{desc.inspect}" end diff --git a/activesupport/lib/active_support/dependencies/autoload.rb b/activesupport/lib/active_support/dependencies/autoload.rb index 9fc58a338f..c0dba5f7fd 100644 --- a/activesupport/lib/active_support/dependencies/autoload.rb +++ b/activesupport/lib/active_support/dependencies/autoload.rb @@ -34,7 +34,7 @@ module ActiveSupport def autoload(const_name, path = @_at_path) unless path - full = [name, @_under_path, const_name.to_s, path].compact.join("::") + full = [name, @_under_path, const_name.to_s].compact.join("::") path = Inflector.underscore(full) end diff --git a/activesupport/lib/active_support/deprecation.rb b/activesupport/lib/active_support/deprecation.rb index 6c15fffc0f..ab16977bda 100644 --- a/activesupport/lib/active_support/deprecation.rb +++ b/activesupport/lib/active_support/deprecation.rb @@ -25,14 +25,14 @@ module ActiveSupport include Reporting include MethodWrapper - # The version the deprecated behavior will be removed, by default. + # The version number in which the deprecated behavior will be removed, by default. attr_accessor :deprecation_horizon - # It accepts two parameters on initialization. The first is an version of library - # and the second is an library name + # It accepts two parameters on initialization. The first is a version of library + # and the second is a library name # # ActiveSupport::Deprecation.new('2.0', 'MyLibrary') - def initialize(deprecation_horizon = '4.1', gem_name = 'Rails') + def initialize(deprecation_horizon = '4.2', 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 90db180124..328b8c320a 100644 --- a/activesupport/lib/active_support/deprecation/behaviors.rb +++ b/activesupport/lib/active_support/deprecation/behaviors.rb @@ -1,14 +1,24 @@ require "active_support/notifications" module ActiveSupport + class DeprecationException < StandardError + end + class Deprecation # Default warning behaviors per Rails.env. DEFAULT_BEHAVIORS = { - :stderr => Proc.new { |message, callstack| + raise: ->(message, callstack) { + e = DeprecationException.new(message) + e.set_backtrace(callstack) + raise e + }, + + stderr: ->(message, callstack) { $stderr.puts(message) $stderr.puts callstack.join("\n ") if debug }, - :log => Proc.new { |message, callstack| + + log: ->(message, callstack) { logger = if defined?(Rails) && Rails.logger Rails.logger @@ -19,11 +29,13 @@ module ActiveSupport logger.warn message logger.debug callstack.join("\n ") if debug }, - :notify => Proc.new { |message, callstack| + + notify: ->(message, callstack) { ActiveSupport::Notifications.instrument("deprecation.rails", :message => message, :callstack => callstack) }, - :silence => Proc.new { |message, callstack| } + + silence: ->(message, callstack) {}, } module Behavior @@ -40,6 +52,7 @@ module ActiveSupport # # Available behaviors: # + # [+raise+] Raise <tt>ActiveSupport::DeprecationException</tt>. # [+stderr+] Log all deprecation warnings to +$stderr+. # [+log+] Log all deprecation warnings to +Rails.logger+. # [+notify+] Use +ActiveSupport::Notifications+ to notify +deprecation.rails+. @@ -52,7 +65,7 @@ module ActiveSupport # ActiveSupport::Deprecation.behavior = :stderr # ActiveSupport::Deprecation.behavior = [:stderr, :log] # ActiveSupport::Deprecation.behavior = MyCustomHandler - # ActiveSupport::Deprecation.behavior = proc { |message, callstack| + # ActiveSupport::Deprecation.behavior = ->(message, callstack) { # # custom stuff # } def behavior=(behavior) diff --git a/activesupport/lib/active_support/deprecation/proxy_wrappers.rb b/activesupport/lib/active_support/deprecation/proxy_wrappers.rb index 485dc91063..a03a66b96b 100644 --- a/activesupport/lib/active_support/deprecation/proxy_wrappers.rb +++ b/activesupport/lib/active_support/deprecation/proxy_wrappers.rb @@ -25,7 +25,7 @@ module ActiveSupport end end - # This DeprecatedObjectProxy transforms object to depracated object. + # This DeprecatedObjectProxy transforms object to deprecated object. # # @old_object = DeprecatedObjectProxy.new(Object.new, "Don't use this object anymore!") # @old_object = DeprecatedObjectProxy.new(Object.new, "Don't use this object anymore!", deprecator_instance) @@ -52,7 +52,7 @@ module ActiveSupport end # This DeprecatedInstanceVariableProxy transforms instance variable to - # depracated instance variable. + # deprecated instance variable. # # class Example # def initialize(deprecator) @@ -93,7 +93,7 @@ module ActiveSupport end end - # This DeprecatedConstantProxy transforms constant to depracated constant. + # This DeprecatedConstantProxy transforms constant to deprecated constant. # # OLD_CONST = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('OLD_CONST', 'NEW_CONST') # OLD_CONST = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('OLD_CONST', 'NEW_CONST', deprecator_instance) diff --git a/activesupport/lib/active_support/duration.rb b/activesupport/lib/active_support/duration.rb index 2cb1f408b6..87b6407038 100644 --- a/activesupport/lib/active_support/duration.rb +++ b/activesupport/lib/active_support/duration.rb @@ -70,13 +70,11 @@ module ActiveSupport alias :until :ago def inspect #:nodoc: - consolidated = parts.inject(::Hash.new(0)) { |h,(l,r)| h[l] += r; h } - parts = [:years, :months, :days, :minutes, :seconds].map do |length| - n = consolidated[length] - "#{n} #{n == 1 ? length.to_s.singularize : length.to_s}" if n.nonzero? - end.compact - parts = ["0 seconds"] if parts.empty? - parts.to_sentence(:locale => :en) + parts. + reduce(::Hash.new(0)) { |h,(l,r)| h[l] += r; h }. + sort_by {|unit, _ | [:years, :months, :days, :minutes, :seconds].index(unit)}. + map {|unit, val| "#{val} #{val == 1 ? unit.to_s.chop : unit.to_s}"}. + to_sentence(:locale => :en) end def as_json(options = nil) #:nodoc: diff --git a/activesupport/lib/active_support/file_update_checker.rb b/activesupport/lib/active_support/file_update_checker.rb index 20136dd1b0..d6918bede2 100644 --- a/activesupport/lib/active_support/file_update_checker.rb +++ b/activesupport/lib/active_support/file_update_checker.rb @@ -115,7 +115,7 @@ module ActiveSupport end def compile_glob(hash) - hash.freeze # Freeze so changes aren't accidently pushed + hash.freeze # Freeze so changes aren't accidentally pushed return if hash.empty? globs = hash.map do |key, value| diff --git a/activesupport/lib/active_support/gzip.rb b/activesupport/lib/active_support/gzip.rb index 6ef33ab683..b837c879bb 100644 --- a/activesupport/lib/active_support/gzip.rb +++ b/activesupport/lib/active_support/gzip.rb @@ -25,9 +25,9 @@ module ActiveSupport end # Compresses a string using gzip. - def self.compress(source) + def self.compress(source, level=Zlib::DEFAULT_COMPRESSION, strategy=Zlib::DEFAULT_STRATEGY) output = Stream.new - gz = Zlib::GzipWriter.new(output) + gz = Zlib::GzipWriter.new(output, level, strategy) gz.write(source) gz.close output.string diff --git a/activesupport/lib/active_support/hash_with_indifferent_access.rb b/activesupport/lib/active_support/hash_with_indifferent_access.rb index 306d80b2df..3da99872c0 100644 --- a/activesupport/lib/active_support/hash_with_indifferent_access.rb +++ b/activesupport/lib/active_support/hash_with_indifferent_access.rb @@ -78,7 +78,7 @@ module ActiveSupport end def self.[](*args) - new.merge(Hash[*args]) + new.merge!(Hash[*args]) end alias_method :regular_writer, :[]= unless method_defined?(:regular_writer) @@ -91,7 +91,7 @@ module ActiveSupport # # This value can be later fetched using either +:key+ or +'key'+. def []=(key, value) - regular_writer(convert_key(key), convert_value(value)) + regular_writer(convert_key(key), convert_value(value, for: :assignment)) end alias_method :store, :[]= @@ -223,13 +223,21 @@ module ActiveSupport def deep_stringify_keys; dup end undef :symbolize_keys! undef :deep_symbolize_keys! - def symbolize_keys; to_hash.symbolize_keys end - def deep_symbolize_keys; to_hash.deep_symbolize_keys end + def symbolize_keys; to_hash.symbolize_keys! end + def deep_symbolize_keys; to_hash.deep_symbolize_keys! end def to_options!; self end + def select(*args, &block) + dup.tap {|hash| hash.select!(*args, &block)} + end + # Convert to a regular hash with string keys. def to_hash - Hash.new(default).merge!(self) + _new_hash= {} + each do |key, value| + _new_hash[convert_key(key)] = convert_value(value, for: :to_hash) + end + Hash.new(default).merge!(_new_hash) end protected @@ -237,12 +245,18 @@ module ActiveSupport key.kind_of?(Symbol) ? key.to_s : key end - def convert_value(value) + def convert_value(value, options = {}) if value.is_a? Hash - value.nested_under_indifferent_access + if options[:for] == :to_hash + value.to_hash + else + value.nested_under_indifferent_access + end elsif value.is_a?(Array) - value = value.dup if value.frozen? - value.map! { |e| convert_value(e) } + unless options[:for] == :assignment + value = value.dup + end + value.map! { |e| convert_value(e, options) } else value end diff --git a/activesupport/lib/active_support/i18n.rb b/activesupport/lib/active_support/i18n.rb index 188653bd9b..6cc98191d4 100644 --- a/activesupport/lib/active_support/i18n.rb +++ b/activesupport/lib/active_support/i18n.rb @@ -1,10 +1,13 @@ +require 'active_support/core_ext/hash/deep_merge' +require 'active_support/core_ext/hash/except' +require 'active_support/core_ext/hash/slice' begin require 'i18n' - require 'active_support/lazy_load_hooks' rescue LoadError => e $stderr.puts "The i18n gem is not available. Please add it to your Gemfile and run bundle install" raise e end +require 'active_support/lazy_load_hooks' ActiveSupport.run_load_hooks(:i18n) I18n.load_path << "#{File.dirname(__FILE__)}/locale/en.yml" diff --git a/activesupport/lib/active_support/inflections.rb b/activesupport/lib/active_support/inflections.rb index ef882ebd09..4ea6abfa12 100644 --- a/activesupport/lib/active_support/inflections.rb +++ b/activesupport/lib/active_support/inflections.rb @@ -57,7 +57,6 @@ module ActiveSupport inflect.irregular('child', 'children') inflect.irregular('sex', 'sexes') inflect.irregular('move', 'moves') - inflect.irregular('cow', 'kine') inflect.irregular('zombie', 'zombies') inflect.uncountable(%w(equipment information rice money species series fish sheep jeans police)) diff --git a/activesupport/lib/active_support/inflector/inflections.rb b/activesupport/lib/active_support/inflector/inflections.rb index 6f259a093b..c96debb93f 100644 --- a/activesupport/lib/active_support/inflector/inflections.rb +++ b/activesupport/lib/active_support/inflector/inflections.rb @@ -1,3 +1,4 @@ +require 'thread_safe' require 'active_support/core_ext/array/prepend_and_append' require 'active_support/i18n' @@ -24,9 +25,10 @@ module ActiveSupport # singularization rules that is runs. This guarantees that your rules run # before any of the rules that may already have been loaded. class Inflections + @__instance__ = ThreadSafe::Cache.new + def self.instance(locale = :en) - @__instance__ ||= Hash.new { |h, k| h[k] = new } - @__instance__[locale] + @__instance__[locale] ||= new end attr_reader :plurals, :singulars, :uncountables, :humans, :acronyms, :acronym_regex @@ -126,17 +128,29 @@ module ActiveSupport def irregular(singular, plural) @uncountables.delete(singular) @uncountables.delete(plural) - if singular[0,1].upcase == plural[0,1].upcase - plural(Regexp.new("(#{singular[0,1]})#{singular[1..-1]}$", "i"), '\1' + plural[1..-1]) - plural(Regexp.new("(#{plural[0,1]})#{plural[1..-1]}$", "i"), '\1' + plural[1..-1]) - singular(Regexp.new("(#{plural[0,1]})#{plural[1..-1]}$", "i"), '\1' + singular[1..-1]) + + s0 = singular[0] + srest = singular[1..-1] + + p0 = plural[0] + prest = plural[1..-1] + + if s0.upcase == p0.upcase + plural(/(#{s0})#{srest}$/i, '\1' + prest) + plural(/(#{p0})#{prest}$/i, '\1' + prest) + + singular(/(#{s0})#{srest}$/i, '\1' + srest) + singular(/(#{p0})#{prest}$/i, '\1' + srest) else - plural(Regexp.new("#{singular[0,1].upcase}(?i)#{singular[1..-1]}$"), plural[0,1].upcase + plural[1..-1]) - plural(Regexp.new("#{singular[0,1].downcase}(?i)#{singular[1..-1]}$"), plural[0,1].downcase + plural[1..-1]) - plural(Regexp.new("#{plural[0,1].upcase}(?i)#{plural[1..-1]}$"), plural[0,1].upcase + plural[1..-1]) - plural(Regexp.new("#{plural[0,1].downcase}(?i)#{plural[1..-1]}$"), plural[0,1].downcase + plural[1..-1]) - singular(Regexp.new("#{plural[0,1].upcase}(?i)#{plural[1..-1]}$"), singular[0,1].upcase + singular[1..-1]) - singular(Regexp.new("#{plural[0,1].downcase}(?i)#{plural[1..-1]}$"), singular[0,1].downcase + singular[1..-1]) + plural(/#{s0.upcase}(?i)#{srest}$/, p0.upcase + prest) + plural(/#{s0.downcase}(?i)#{srest}$/, p0.downcase + prest) + plural(/#{p0.upcase}(?i)#{prest}$/, p0.upcase + prest) + plural(/#{p0.downcase}(?i)#{prest}$/, p0.downcase + prest) + + singular(/#{s0.upcase}(?i)#{srest}$/, s0.upcase + srest) + singular(/#{s0.downcase}(?i)#{srest}$/, s0.downcase + srest) + singular(/#{p0.upcase}(?i)#{prest}$/, s0.upcase + srest) + singular(/#{p0.downcase}(?i)#{prest}$/, s0.downcase + srest) end end diff --git a/activesupport/lib/active_support/inflector/methods.rb b/activesupport/lib/active_support/inflector/methods.rb index 1eb2b4212b..ffdb7b53c4 100644 --- a/activesupport/lib/active_support/inflector/methods.rb +++ b/activesupport/lib/active_support/inflector/methods.rb @@ -1,6 +1,5 @@ # encoding: utf-8 -require 'active_support/inflector/inflections' require 'active_support/inflections' module ActiveSupport @@ -37,7 +36,7 @@ module ActiveSupport # string. # # If passed an optional +locale+ parameter, the word will be - # pluralized using rules defined for that language. By default, + # singularized using rules defined for that language. By default, # this parameter is set to <tt>:en</tt>. # # 'posts'.singularize # => "post" @@ -73,7 +72,9 @@ module ActiveSupport else string = string.sub(/^(?:#{inflections.acronym_regex}(?=\b|[A-Z_])|\w)/) { $&.downcase } end - string.gsub(/(?:_|(\/))([a-z\d]*)/i) { "#{$1}#{inflections.acronyms[$2] || $2.capitalize}" }.gsub('/', '::') + string.gsub!(/(?:_|(\/))([a-z\d]*)/i) { "#{$1}#{inflections.acronyms[$2] || $2.capitalize}" } + string.gsub!('/', '::') + string end # Makes an underscored, lowercase form from the expression in the string. @@ -88,8 +89,7 @@ module ActiveSupport # # 'SSLError'.underscore.camelize # => "SslError" def underscore(camel_cased_word) - word = camel_cased_word.to_s.dup - word.gsub!('::', '/') + word = camel_cased_word.to_s.gsub('::', '/') word.gsub!(/(?:([A-Za-z\d])|^)(#{inflections.acronym_regex})(?=\b|[^a-z])/) { "#{$1}#{$1 && '_'}#{$2.downcase}" } word.gsub!(/([A-Z\d]+)([A-Z][a-z])/,'\1_\2') word.gsub!(/([a-z\d])([A-Z])/,'\1_\2') @@ -185,7 +185,7 @@ module ActiveSupport # # See also +demodulize+. def deconstantize(path) - path.to_s[0...(path.rindex('::') || 0)] # implementation based on the one in facets' Module#spacename + path.to_s[0, path.rindex('::') || 0] # implementation based on the one in facets' Module#spacename end # Creates a foreign key name from a class name. @@ -219,7 +219,12 @@ module ActiveSupport # unknown. def constantize(camel_cased_word) names = camel_cased_word.split('::') - names.shift if names.empty? || names.first.empty? + + # Trigger a builtin NameError exception including the ill-formed constant in the message. + Object.const_get(camel_cased_word) if names.empty? + + # Remove the first blank element in case of '::ClassName' notation. + names.shift if names.size > 1 && names.first.empty? names.inject(Object) do |constant, name| if constant == Object @@ -266,14 +271,12 @@ module ActiveSupport # 'UnknownModule'.safe_constantize # => nil # 'UnknownModule::Foo::Bar'.safe_constantize # => nil def safe_constantize(camel_cased_word) - begin - constantize(camel_cased_word) - rescue NameError => e - raise unless e.message =~ /(uninitialized constant|wrong constant name) #{const_regexp(camel_cased_word)}$/ || - e.name.to_s == camel_cased_word.to_s - rescue ArgumentError => e - raise unless e.message =~ /not missing constant #{const_regexp(camel_cased_word)}\!$/ - end + constantize(camel_cased_word) + rescue NameError => e + raise unless e.message =~ /(uninitialized constant|wrong constant name) #{const_regexp(camel_cased_word)}$/ || + e.name.to_s == camel_cased_word.to_s + rescue ArgumentError => e + raise unless e.message =~ /not missing constant #{const_regexp(camel_cased_word)}\!$/ end # Returns the suffix that should be added to a number to denote the position @@ -316,9 +319,14 @@ module ActiveSupport private # Mount a regular expression that will match part by part of the constant. - # For instance, Foo::Bar::Baz will generate Foo(::Bar(::Baz)?)? + # + # const_regexp("Foo::Bar::Baz") # => /Foo(::Bar(::Baz)?)?/ + # const_regexp("::") # => /::/ def const_regexp(camel_cased_word) #:nodoc: parts = camel_cased_word.split("::") + + return Regexp.escape(camel_cased_word) if parts.blank? + last = parts.pop parts.reverse.inject(last) do |acc, part| diff --git a/activesupport/lib/active_support/json/decoding.rb b/activesupport/lib/active_support/json/decoding.rb index a4a32b2ad0..21de09c1cc 100644 --- a/activesupport/lib/active_support/json/decoding.rb +++ b/activesupport/lib/active_support/json/decoding.rb @@ -1,6 +1,6 @@ require 'active_support/core_ext/module/attribute_accessors' require 'active_support/core_ext/module/delegation' -require 'multi_json' +require 'json' module ActiveSupport # Look for and parse json strings that look like ISO 8601 times. @@ -13,8 +13,8 @@ module ActiveSupport # # ActiveSupport::JSON.decode("{\"team\":\"rails\",\"players\":\"36\"}") # => {"team" => "rails", "players" => "36"} - def decode(json, options ={}) - data = MultiJson.load(json, options) + def decode(json, options = {}) + data = ::JSON.parse(json, options.merge(create_additions: false, quirks_mode: true)) if ActiveSupport.parse_json_times convert_dates_from(data) else @@ -22,23 +22,6 @@ module ActiveSupport end end - def engine - MultiJson.adapter - end - alias :backend :engine - - def engine=(name) - MultiJson.use(name) - end - alias :backend= :engine= - - def with_backend(name) - old_backend, self.backend = backend, name - yield - ensure - self.backend = old_backend - end - # Returns the class of the error that will be raised when there is an # error in decoding JSON. Using this method means you won't directly # depend on the ActiveSupport's JSON implementation, in case it changes @@ -50,7 +33,7 @@ module ActiveSupport # Rails.logger.warn("Attempted to decode invalid JSON: #{some_string}") # end def parse_error - MultiJson::DecodeError + ::JSON::ParserError end private diff --git a/activesupport/lib/active_support/json/encoding.rb b/activesupport/lib/active_support/json/encoding.rb index 7a5c351ca8..77b5d8d227 100644 --- a/activesupport/lib/active_support/json/encoding.rb +++ b/activesupport/lib/active_support/json/encoding.rb @@ -1,6 +1,7 @@ +#encoding: us-ascii + require 'active_support/core_ext/object/to_json' require 'active_support/core_ext/module/delegation' -require 'active_support/json/variable' require 'bigdecimal' require 'active_support/core_ext/big_decimal/conversions' # for #to_s @@ -98,13 +99,18 @@ module ActiveSupport "\010" => '\b', "\f" => '\f', "\n" => '\n', + "\xe2\x80\xa8" => '\u2028', + "\xe2\x80\xa9" => '\u2029', "\r" => '\r', "\t" => '\t', '"' => '\"', '\\' => '\\\\', '>' => '\u003E', '<' => '\u003C', - '&' => '\u0026' } + '&' => '\u0026', + "#{0xe2.chr}#{0x80.chr}#{0xa8.chr}" => '\u2028', + "#{0xe2.chr}#{0x80.chr}#{0xa9.chr}" => '\u2029', + } class << self # If true, use ISO 8601 format for dates and times. Otherwise, fall back @@ -121,21 +127,15 @@ module ActiveSupport def escape_html_entities_in_json=(value) self.escape_regex = \ if @escape_html_entities_in_json = value - /[\x00-\x1F"\\><&]/ + /\xe2\x80\xa8|\xe2\x80\xa9|[\x00-\x1F"\\><&]/ else - /[\x00-\x1F"\\]/ + /\xe2\x80\xa8|\xe2\x80\xa9|[\x00-\x1F"\\]/ end end def escape(string) string = string.encode(::Encoding::UTF_8, :undef => :replace).force_encoding(::Encoding::BINARY) - json = string. - gsub(escape_regex) { |s| ESCAPED_CHARS[s] }. - gsub(/([\xC0-\xDF][\x80-\xBF]| - [\xE0-\xEF][\x80-\xBF]{2}| - [\xF0-\xF7][\x80-\xBF]{3})+/nx) { |s| - s.unpack("U*").pack("n*").unpack("H*")[0].gsub(/.{4}/n, '\\\\u\&') - } + json = string.gsub(escape_regex) { |s| ESCAPED_CHARS[s] } json = %("#{json}") json.force_encoding(::Encoding::UTF_8) json @@ -241,7 +241,7 @@ class BigDecimal # real value. # # Use <tt>ActiveSupport.use_standard_json_big_decimal_format = true</tt> to - # override this behaviour. + # override this behavior. def as_json(options = nil) #:nodoc: if finite? ActiveSupport.encode_big_decimal_as_string ? to_s : self diff --git a/activesupport/lib/active_support/json/variable.rb b/activesupport/lib/active_support/json/variable.rb deleted file mode 100644 index d69dab6408..0000000000 --- a/activesupport/lib/active_support/json/variable.rb +++ /dev/null @@ -1,18 +0,0 @@ -require 'active_support/deprecation' - -module ActiveSupport - module JSON - # Deprecated: A string that returns itself as its JSON-encoded form. - class Variable < String - def initialize(*args) - message = 'ActiveSupport::JSON::Variable is deprecated and will be removed in Rails 4.1. ' \ - 'For your own custom JSON literals, define #as_json and #encode_json yourself.' - ActiveSupport::Deprecation.warn message - super - end - - def as_json(options = nil) self end #:nodoc: - def encode_json(encoder) self end #:nodoc: - end - end -end diff --git a/activesupport/lib/active_support/key_generator.rb b/activesupport/lib/active_support/key_generator.rb index 6beb2b6afa..598c46bce5 100644 --- a/activesupport/lib/active_support/key_generator.rb +++ b/activesupport/lib/active_support/key_generator.rb @@ -1,10 +1,10 @@ -require 'mutex_m' +require 'thread_safe' require 'openssl' module ActiveSupport # KeyGenerator is a simple wrapper around OpenSSL's implementation of PBKDF2 # It can be used to derive a number of keys for various purposes from a given secret. - # This lets rails applications have a single secure secret, but avoid reusing that + # This lets Rails applications have a single secure secret, but avoid reusing that # key in multiple incompatible contexts. class KeyGenerator def initialize(secret, options = {}) @@ -28,20 +28,18 @@ module ActiveSupport class CachingKeyGenerator def initialize(key_generator) @key_generator = key_generator - @cache_keys = {}.extend(Mutex_m) + @cache_keys = ThreadSafe::Cache.new end # Returns a derived key suitable for use. The default key_size is chosen # to be compatible with the default settings of ActiveSupport::MessageVerifier. # i.e. OpenSSL::Digest::SHA1#block_length def generate_key(salt, key_size=64) - @cache_keys.synchronize do - @cache_keys["#{salt}#{key_size}"] ||= @key_generator.generate_key(salt, key_size) - end + @cache_keys["#{salt}#{key_size}"] ||= @key_generator.generate_key(salt, key_size) end end - class DummyKeyGenerator # :nodoc: + class LegacyKeyGenerator # :nodoc: SECRET_MIN_LENGTH = 30 # Characters def initialize(secret) diff --git a/activesupport/lib/active_support/lazy_load_hooks.rb b/activesupport/lib/active_support/lazy_load_hooks.rb index e489512531..e2b8f0f648 100644 --- a/activesupport/lib/active_support/lazy_load_hooks.rb +++ b/activesupport/lib/active_support/lazy_load_hooks.rb @@ -1,5 +1,5 @@ module ActiveSupport - # lazy_load_hooks allows rails to lazily load a lot of components and thus + # lazy_load_hooks allows Rails to lazily load a lot of components and thus # making the app boot faster. Because of this feature now there is no need to # require <tt>ActiveRecord::Base</tt> at boot time purely to apply # configuration. Instead a hook is registered that applies configuration once diff --git a/activesupport/lib/active_support/locale/en.yml b/activesupport/lib/active_support/locale/en.yml index f4900dc935..a4563ace8f 100644 --- a/activesupport/lib/active_support/locale/en.yml +++ b/activesupport/lib/active_support/locale/en.yml @@ -16,9 +16,9 @@ en: abbr_month_names: [~, Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec] # Used in date_select and datetime_select. order: - - :year - - :month - - :day + - year + - month + - day time: formats: diff --git a/activesupport/lib/active_support/log_subscriber.rb b/activesupport/lib/active_support/log_subscriber.rb index a58afc6b9d..e95dc5a866 100644 --- a/activesupport/lib/active_support/log_subscriber.rb +++ b/activesupport/lib/active_support/log_subscriber.rb @@ -1,5 +1,6 @@ require 'active_support/core_ext/module/attribute_accessors' require 'active_support/core_ext/class/attribute' +require 'active_support/subscriber' module ActiveSupport # ActiveSupport::LogSubscriber is an object set to consume @@ -33,7 +34,7 @@ module ActiveSupport # 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. - class LogSubscriber + class LogSubscriber < Subscriber # Embed in a String to clear all previous ANSI sequences. CLEAR = "\e[0m" BOLD = "\e[1m" @@ -53,24 +54,15 @@ module ActiveSupport class << self def logger - @logger ||= Rails.logger if defined?(Rails) - @logger + @logger ||= if defined?(Rails) && Rails.respond_to?(:logger) + Rails.logger + end end attr_writer :logger - def attach_to(namespace, log_subscriber=new, notifier=ActiveSupport::Notifications) - log_subscribers << log_subscriber - - log_subscriber.public_methods(false).each do |event| - next if %w{ start finish }.include?(event.to_s) - - notifier.subscribe("#{event}.#{namespace}", log_subscriber) - end - end - def log_subscribers - @@log_subscribers ||= [] + subscribers end # Flush all log_subscribers' logger. @@ -79,39 +71,18 @@ module ActiveSupport end end - def initialize - @queue_key = [self.class.name, object_id].join "-" - super - end - def logger LogSubscriber.logger end def start(name, id, payload) - return unless logger - - e = ActiveSupport::Notifications::Event.new(name, Time.now, nil, id, payload) - parent = event_stack.last - parent << e if parent - - event_stack.push e + super if logger end def finish(name, id, payload) - return unless logger - - finished = Time.now - event = event_stack.pop - event.end = finished - event.payload.merge!(payload) - - method = name.split('.').first - begin - send(method, event) - rescue Exception => e - logger.error "Could not log #{name.inspect} event. #{e.class}: #{e.message} #{e.backtrace}" - end + super if logger + rescue Exception => e + logger.error "Could not log #{name.inspect} event. #{e.class}: #{e.message} #{e.backtrace}" end protected @@ -134,11 +105,5 @@ module ActiveSupport bold = bold ? BOLD : "" "#{bold}#{color}#{text}#{CLEAR}" end - - private - - def event_stack - Thread.current[@queue_key] ||= [] - end end end diff --git a/activesupport/lib/active_support/log_subscriber/test_helper.rb b/activesupport/lib/active_support/log_subscriber/test_helper.rb index 63dad7e01a..75f353f62c 100644 --- a/activesupport/lib/active_support/log_subscriber/test_helper.rb +++ b/activesupport/lib/active_support/log_subscriber/test_helper.rb @@ -1,5 +1,5 @@ require 'active_support/log_subscriber' -require 'active_support/buffered_logger' +require 'active_support/logger' require 'active_support/notifications' module ActiveSupport @@ -15,7 +15,7 @@ module ActiveSupport # end # # def test_basic_query_logging - # Developer.all + # Developer.all.to_a # wait # assert_equal 1, @logger.logged(:debug).size # assert_match(/Developer Load/, @logger.logged(:debug).last) diff --git a/activesupport/lib/active_support/logger.rb b/activesupport/lib/active_support/logger.rb index 65202f99fc..4a55bbb350 100644 --- a/activesupport/lib/active_support/logger.rb +++ b/activesupport/lib/active_support/logger.rb @@ -1,7 +1,11 @@ +require 'active_support/core_ext/class/attribute_accessors' +require 'active_support/logger_silence' require 'logger' module ActiveSupport class Logger < ::Logger + include LoggerSilence + # Broadcasts logs to multiple loggers. def self.broadcast(logger) # :nodoc: Module.new do diff --git a/activesupport/lib/active_support/logger_silence.rb b/activesupport/lib/active_support/logger_silence.rb new file mode 100644 index 0000000000..a8efdef944 --- /dev/null +++ b/activesupport/lib/active_support/logger_silence.rb @@ -0,0 +1,24 @@ +require 'active_support/concern' + +module LoggerSilence + extend ActiveSupport::Concern + + included do + cattr_accessor :silencer + self.silencer = true + 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 + yield self + ensure + self.level = old_logger_level + end + else + yield self + end + end +end
\ No newline at end of file diff --git a/activesupport/lib/active_support/message_encryptor.rb b/activesupport/lib/active_support/message_encryptor.rb index b7dc0689b0..bffdfc6201 100644 --- a/activesupport/lib/active_support/message_encryptor.rb +++ b/activesupport/lib/active_support/message_encryptor.rb @@ -1,5 +1,6 @@ require 'openssl' require 'base64' +require 'active_support/core_ext/array/extract_options' module ActiveSupport # MessageEncryptor is a simple way to encrypt values which get stored @@ -11,10 +12,11 @@ module ActiveSupport # This can be used in situations similar to the <tt>MessageVerifier</tt>, but # where you don't want users to be able to determine the value of the payload. # - # key = OpenSSL::Digest::SHA256.new('password').digest # => "\x89\xE0\x156\xAC..." - # crypt = ActiveSupport::MessageEncryptor.new(key) # => #<ActiveSupport::MessageEncryptor ...> - # encrypted_data = crypt.encrypt_and_sign('my secret data') # => "NlFBTTMwOUV5UlA1QlNEN2xkY2d6eThYWWh..." - # crypt.decrypt_and_verify(encrypted_data) # => "my secret data" + # salt = SecureRandom.random_bytes(64) + # key = ActiveSupport::KeyGenerator.new('password').generate_key(salt) # => "\x89\xE0\x156\xAC..." + # crypt = ActiveSupport::MessageEncryptor.new(key) # => #<ActiveSupport::MessageEncryptor ...> + # encrypted_data = crypt.encrypt_and_sign('my secret data') # => "NlFBTTMwOUV5UlA1QlNEN2xkY2d6eThYWWh..." + # crypt.decrypt_and_verify(encrypted_data) # => "my secret data" class MessageEncryptor module NullSerializer #:nodoc: def self.load(value) @@ -27,7 +29,7 @@ module ActiveSupport end class InvalidMessage < StandardError; end - OpenSSLCipherError = OpenSSL::Cipher.const_defined?(:CipherError) ? OpenSSL::Cipher::CipherError : OpenSSL::CipherError + OpenSSLCipherError = OpenSSL::Cipher::CipherError # Initialize a new MessageEncryptor. +secret+ must be at least as long as # the cipher key size. For the default 'aes-256-cbc' cipher, this is 256 @@ -65,12 +67,11 @@ module ActiveSupport def _encrypt(value) cipher = new_cipher - # Rely on OpenSSL for the initialization vector - iv = cipher.random_iv - cipher.encrypt cipher.key = @secret - cipher.iv = iv + + # Rely on OpenSSL for the initialization vector + iv = cipher.random_iv encrypted_data = cipher.update(@serializer.dump(value)) encrypted_data << cipher.final diff --git a/activesupport/lib/active_support/message_verifier.rb b/activesupport/lib/active_support/message_verifier.rb index a87383fe99..e0cd92ae3c 100644 --- a/activesupport/lib/active_support/message_verifier.rb +++ b/activesupport/lib/active_support/message_verifier.rb @@ -19,10 +19,10 @@ module ActiveSupport # end # # By default it uses Marshal to serialize the message. If you want to use - # another serialization method, you can set the serializer attribute to - # something that responds to dump and load, e.g.: + # another serialization method, you can set the serializer in the options + # hash upon initialization: # - # @verifier.serializer = YAML + # @verifier = ActiveSupport::MessageVerifier.new('s3Krit', serializer: YAML) class MessageVerifier class InvalidSignature < StandardError; end diff --git a/activesupport/lib/active_support/multibyte/chars.rb b/activesupport/lib/active_support/multibyte/chars.rb index a42e7f6542..3c0cf9f137 100644 --- a/activesupport/lib/active_support/multibyte/chars.rb +++ b/activesupport/lib/active_support/multibyte/chars.rb @@ -56,11 +56,10 @@ module ActiveSupport #:nodoc: # Forward all undefined methods to the wrapped string. def method_missing(method, *args, &block) + result = @wrapped_string.__send__(method, *args, &block) if method.to_s =~ /!$/ - result = @wrapped_string.__send__(method, *args, &block) self if result else - result = @wrapped_string.__send__(method, *args, &block) result.kind_of?(String) ? chars(result) : result end end diff --git a/activesupport/lib/active_support/multibyte/unicode.rb b/activesupport/lib/active_support/multibyte/unicode.rb index f49ca47f14..1845c6ae38 100644 --- a/activesupport/lib/active_support/multibyte/unicode.rb +++ b/activesupport/lib/active_support/multibyte/unicode.rb @@ -11,7 +11,7 @@ module ActiveSupport NORMALIZATION_FORMS = [:c, :kc, :d, :kd] # The Unicode version that is supported by the implementation - UNICODE_VERSION = '6.1.0' + UNICODE_VERSION = '6.2.0' # The default normalization used for operations that require # normalization. It can be set to any of the normalizations @@ -145,7 +145,7 @@ module ActiveSupport 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) and (!database.codepoints[cp].decomp_type || type == :compatability) + elsif (ncp = database.codepoints[cp].decomp_mapping) and (!database.codepoints[cp].decomp_type || type == :compatibility) decomposed.concat decompose(type, ncp.dup) else decomposed << cp @@ -218,51 +218,31 @@ module ActiveSupport # Passing +true+ will forcibly tidy all bytes, assuming that the string's # encoding is entirely CP1252 or ISO-8859-1. def tidy_bytes(string, force = false) + return string if string.empty? + if force - return string.unpack("C*").map do |b| - tidy_byte(b) - end.flatten.compact.pack("C*").unpack("U*").pack("U*") + return string.encode(Encoding::UTF_8, Encoding::Windows_1252, invalid: :replace, undef: :replace) end - bytes = string.unpack("C*") - conts_expected = 0 - last_lead = 0 - - bytes.each_index do |i| + # We can't transcode to the same format, so we choose a nearly-identical encoding. + # We're going to 'transcode' bytes from UTF-8 when possible, then fall back to + # CP1252 when we get errors. The final string will be 'converted' back to UTF-8 + # before returning. + reader = Encoding::Converter.new(Encoding::UTF_8, Encoding::UTF_8_MAC) - byte = bytes[i] - is_cont = byte > 127 && byte < 192 - is_lead = byte > 191 && byte < 245 - is_unused = byte > 240 - is_restricted = byte > 244 + source = string.dup + out = ''.force_encoding(Encoding::UTF_8_MAC) - # Impossible or highly unlikely byte? Clean it. - if is_unused || is_restricted - bytes[i] = tidy_byte(byte) - elsif is_cont - # Not expecting continuation byte? Clean up. Otherwise, now expect one less. - conts_expected == 0 ? bytes[i] = tidy_byte(byte) : conts_expected -= 1 - else - if conts_expected > 0 - # Expected continuation, but got ASCII or leading? Clean backwards up to - # the leading byte. - (1..(i - last_lead)).each {|j| bytes[i - j] = tidy_byte(bytes[i - j])} - conts_expected = 0 - end - if is_lead - # Final byte is leading? Clean it. - if i == bytes.length - 1 - bytes[i] = tidy_byte(bytes.last) - else - # Valid leading byte? Expect continuations determined by position of - # first zero bit, with max of 3. - conts_expected = byte < 224 ? 1 : byte < 240 ? 2 : 3 - last_lead = i - end - end - end + loop do + reader.primitive_convert(source, out) + _, _, _, error_bytes, _ = reader.primitive_errinfo + break if error_bytes.nil? + out << error_bytes.encode(Encoding::UTF_8_MAC, Encoding::Windows_1252, invalid: :replace, undef: :replace) end - bytes.empty? ? "" : bytes.flatten.compact.pack("C*").unpack("U*").pack("U*") + + reader.finish + + out.encode!(Encoding::UTF_8) end # Returns the KC normalization of the string by default. NFKC is @@ -283,9 +263,9 @@ module ActiveSupport when :c compose(reorder_characters(decompose(:canonical, codepoints))) when :kd - reorder_characters(decompose(:compatability, codepoints)) + reorder_characters(decompose(:compatibility, codepoints)) when :kc - compose(reorder_characters(decompose(:compatability, codepoints))) + compose(reorder_characters(decompose(:compatibility, codepoints))) else raise ArgumentError, "#{form} is not a valid normalization variant", caller end.pack('U*') @@ -307,6 +287,13 @@ module ActiveSupport class Codepoint attr_accessor :code, :combining_class, :decomp_type, :decomp_mapping, :uppercase_mapping, :lowercase_mapping + # Initializing Codepoint object with default values + def initialize + @combining_class = 0 + @uppercase_mapping = 0 + @lowercase_mapping = 0 + end + def swapcase_mapping uppercase_mapping > 0 ? uppercase_mapping : lowercase_mapping end diff --git a/activesupport/lib/active_support/notifications.rb b/activesupport/lib/active_support/notifications.rb index 705a4693b7..b32aa75e59 100644 --- a/activesupport/lib/active_support/notifications.rb +++ b/activesupport/lib/active_support/notifications.rb @@ -1,5 +1,6 @@ require 'active_support/notifications/instrumenter' require 'active_support/notifications/fanout' +require 'active_support/per_thread_registry' module ActiveSupport # = Notifications @@ -142,8 +143,8 @@ module ActiveSupport # # == Default Queue # - # Notifications ships with a queue implementation that consumes and publish events - # to log subscribers in a thread. You can use any queue implementation you want. + # Notifications ships with a queue implementation that consumes and publishes events + # to all log subscribers. You can use any queue implementation you want. # module Notifications class << self @@ -177,7 +178,27 @@ module ActiveSupport end def instrumenter - Thread.current[:"instrumentation_#{notifier.object_id}"] ||= Instrumenter.new(notifier) + InstrumentationRegistry.instrumenter_for(notifier) + end + end + + # This class is a registry which holds all of the +Instrumenter+ objects + # in a particular thread local. To access the +Instrumenter+ object for a + # particular +notifier+, you can call the following method: + # + # InstrumentationRegistry.instrumenter_for(notifier) + # + # The instrumenters for multiple notifiers are held in a single instance of + # this class. + class InstrumentationRegistry # :nodoc: + extend ActiveSupport::PerThreadRegistry + + def initialize + @registry = {} + end + + def instrumenter_for(notifier) + @registry[notifier] ||= Instrumenter.new(notifier) end end diff --git a/activesupport/lib/active_support/notifications/fanout.rb b/activesupport/lib/active_support/notifications/fanout.rb index 2e5bcf4639..99fe03e6d0 100644 --- a/activesupport/lib/active_support/notifications/fanout.rb +++ b/activesupport/lib/active_support/notifications/fanout.rb @@ -1,4 +1,5 @@ require 'mutex_m' +require 'thread_safe' module ActiveSupport module Notifications @@ -11,7 +12,7 @@ module ActiveSupport def initialize @subscribers = [] - @listeners_for = {} + @listeners_for = ThreadSafe::Cache.new super end @@ -44,7 +45,9 @@ module ActiveSupport end def listeners_for(name) - synchronize do + # this is correctly done double-checked locking (ThreadSafe::Cache'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) } end end @@ -76,6 +79,13 @@ module ActiveSupport def initialize(pattern, delegate) @pattern = pattern @delegate = delegate + @can_publish = delegate.respond_to?(:publish) + end + + def publish(name, *args) + if @can_publish + @delegate.publish name, *args + end end def start(name, id, payload) diff --git a/activesupport/lib/active_support/notifications/instrumenter.rb b/activesupport/lib/active_support/notifications/instrumenter.rb index ab0b162ee0..3a244b34b5 100644 --- a/activesupport/lib/active_support/notifications/instrumenter.rb +++ b/activesupport/lib/active_support/notifications/instrumenter.rb @@ -2,12 +2,12 @@ require 'securerandom' module ActiveSupport module Notifications - # Instrumentors are stored in a thread local. + # Instrumenters are stored in a thread local. class Instrumenter attr_reader :id def initialize(notifier) - @id = unique_id + @id = unique_id @notifier = notifier end @@ -15,21 +15,32 @@ module ActiveSupport # and publish it. Notice that events get sent even if an error occurs # in the passed-in block. def instrument(name, payload={}) - @notifier.start(name, @id, payload) + start name, payload begin - yield + yield payload rescue Exception => e payload[:exception] = [e.class.name, e.message] raise e ensure - @notifier.finish(name, @id, payload) + finish name, payload end end + # Send a start notification with +name+ and +payload+. + def start(name, payload) + @notifier.start name, @id, payload + end + + # Send a finish notification with +name+ and +payload+. + def finish(name, payload) + @notifier.finish name, @id, payload + end + private - def unique_id - SecureRandom.hex(10) - end + + def unique_id + SecureRandom.hex(10) + end end class Event @@ -43,10 +54,11 @@ module ActiveSupport @transaction_id = transaction_id @end = ending @children = [] + @duration = nil end def duration - 1000.0 * (self.end - time) + @duration ||= 1000.0 * (self.end - time) end def <<(event) diff --git a/activesupport/lib/active_support/number_helper.rb b/activesupport/lib/active_support/number_helper.rb index 2191471daa..e0151baa36 100644 --- a/activesupport/lib/active_support/number_helper.rb +++ b/activesupport/lib/active_support/number_helper.rb @@ -108,7 +108,7 @@ module ActiveSupport DECIMAL_UNITS = { 0 => :unit, 1 => :ten, 2 => :hundred, 3 => :thousand, 6 => :million, 9 => :billion, 12 => :trillion, 15 => :quadrillion, -1 => :deci, -2 => :centi, -3 => :mili, -6 => :micro, -9 => :nano, -12 => :pico, -15 => :femto } - + INVERTED_DECIMAL_UNITS = DECIMAL_UNITS.invert STORAGE_UNITS = [:byte, :kb, :mb, :gb, :tb] # Formats a +number+ into a US phone number (e.g., (555) @@ -244,14 +244,14 @@ module ActiveSupport # # ==== Examples # - # number_to_percentage(100) # => 100.000% - # number_to_percentage('98') # => 98.000% - # number_to_percentage(100, precision: 0) # => 100% - # number_to_percentage(1000, delimiter: '.', separator: ,') # => 1.000,000% - # number_to_percentage(302.24398923423, precision: 5) # => 302.24399% - # number_to_percentage(1000, locale: :fr) # => 1 000,000% - # number_to_percentage('98a') # => 98a% - # number_to_percentage(100, format: '%n %') # => 100 % + # number_to_percentage(100) # => 100.000% + # number_to_percentage('98') # => 98.000% + # number_to_percentage(100, precision: 0) # => 100% + # number_to_percentage(1000, delimiter: '.', separator: ',') # => 1.000,000% + # number_to_percentage(302.24398923423, precision: 5) # => 302.24399% + # number_to_percentage(1000, locale: :fr) # => 1 000,000% + # number_to_percentage('98a') # => 98a% + # number_to_percentage(100, format: '%n %') # => 100 % def number_to_percentage(number, options = {}) return unless number options = options.symbolize_keys @@ -295,7 +295,7 @@ module ActiveSupport options = format_options(options[:locale]).merge!(options) - parts = number.to_s.to_str.split('.') + parts = number.to_s.split('.') parts[0].gsub!(/(\d)(?=(\d\d\d)+(?!\d))/, "\\1#{options[:delimiter]}") parts.join(options[:separator]) end @@ -356,7 +356,8 @@ module ActiveSupport digits, rounded_number = 1, 0 else digits = (Math.log10(number.abs) + 1).floor - rounded_number = (BigDecimal.new(number.to_s) / BigDecimal.new((10 ** (digits - precision)).to_f.to_s)).round.to_f * 10 ** (digits - precision) + multiplier = 10 ** (digits - precision) + rounded_number = (BigDecimal.new(number.to_s) / BigDecimal.new(multiplier.to_f.to_s)).round.to_f * multiplier digits = (Math.log10(rounded_number.abs) + 1).floor # After rounding, the number of digits may have changed end precision -= digits @@ -459,7 +460,7 @@ module ActiveSupport # See <tt>number_to_human_size</tt> if you want to print a file # size. # - # You can also define you own unit-quantifier names if you want + # You can also define your own unit-quantifier names if you want # to use other decimal units (eg.: 1500 becomes "1.5 # kilometers", 0.150 becomes "150 milliliters", etc). You may # define a wide range of unit quantifiers, even fractional ones @@ -560,8 +561,6 @@ module ActiveSupport #for backwards compatibility with those that didn't add strip_insignificant_zeros to their locale files options[:strip_insignificant_zeros] = true if not options.key?(:strip_insignificant_zeros) - inverted_du = DECIMAL_UNITS.invert - units = options.delete :units unit_exponents = case units when Hash @@ -572,7 +571,7 @@ module ActiveSupport translate_number_value_with_default("human.decimal_units.units", :locale => options[:locale], :raise => true) else raise ArgumentError, ":units must be a Hash or String translation scope." - end.keys.map{|e_name| inverted_du[e_name] }.sort_by{|e| -e} + end.keys.map!{|e_name| INVERTED_DECIMAL_UNITS[e_name] }.sort_by!{|e| -e} number_exponent = number != 0 ? Math.log10(number.abs).floor : 0 display_exponent = unit_exponents.find{ |e| number_exponent >= e } || 0 @@ -580,7 +579,7 @@ module ActiveSupport unit = case units when Hash - units[DECIMAL_UNITS[display_exponent]] + units[DECIMAL_UNITS[display_exponent]] || '' when String, Symbol I18n.translate(:"#{units}.#{DECIMAL_UNITS[display_exponent]}", :locale => options[:locale], :count => number.to_i) else diff --git a/activesupport/lib/active_support/ordered_options.rb b/activesupport/lib/active_support/ordered_options.rb index c9518bda79..a33e2c58a9 100644 --- a/activesupport/lib/active_support/ordered_options.rb +++ b/activesupport/lib/active_support/ordered_options.rb @@ -40,6 +40,14 @@ module ActiveSupport end end + # +InheritableOptions+ provides a constructor to build an +OrderedOptions+ + # hash inherited from another hash. + # + # Use this if you already have some hash and you want to create a new one based on it. + # + # h = ActiveSupport::InheritableOptions.new({ girl: 'Mary', boy: 'John' }) + # h.girl # => 'Mary' + # h.boy # => 'John' class InheritableOptions < OrderedOptions def initialize(parent = nil) if parent.kind_of?(OrderedOptions) diff --git a/activesupport/lib/active_support/per_thread_registry.rb b/activesupport/lib/active_support/per_thread_registry.rb new file mode 100644 index 0000000000..aa682fb36c --- /dev/null +++ b/activesupport/lib/active_support/per_thread_registry.rb @@ -0,0 +1,52 @@ +module ActiveSupport + # This module is used to encapsulate access to thread local variables. + # + # Instead of polluting the thread locals namespace: + # + # Thread.current[:connection_handler] + # + # you define a class that extends this module: + # + # module ActiveRecord + # class RuntimeRegistry + # extend ActiveSupport::PerThreadRegistry + # + # attr_accessor :connection_handler + # end + # end + # + # and invoke the declared instance accessors as class methods. So + # + # ActiveRecord::RuntimeRegistry.connection_handler = connection_handler + # + # sets a connection handler local to the current thread, and + # + # ActiveRecord::RuntimeRegistry.connection_handler + # + # returns a connection handler local to the current thread. + # + # This feature is accomplished by instantiating the class and storing the + # instance as a thread local keyed by the class name. In the example above + # a key "ActiveRecord::RuntimeRegistry" is stored in <tt>Thread.current</tt>. + # The class methods proxy to said thread local instance. + # + # If the class has an initializer, it must accept no arguments. + module PerThreadRegistry + protected + + def method_missing(name, *args, &block) # :nodoc: + # Caches the method definition as a singleton method of the receiver. + define_singleton_method(name) do |*a, &b| + per_thread_registry_instance.public_send(name, *a, &b) + end + + send(name, *args, &block) + end + + private + + def per_thread_registry_instance + Thread.current[name] ||= new + end + end +end diff --git a/activesupport/lib/active_support/proxy_object.rb b/activesupport/lib/active_support/proxy_object.rb index a2bdf1d790..20a0fd8e62 100644 --- a/activesupport/lib/active_support/proxy_object.rb +++ b/activesupport/lib/active_support/proxy_object.rb @@ -5,7 +5,7 @@ module ActiveSupport undef_method :== undef_method :equal? - # Let ActiveSupport::BasicObject at least raise exceptions. + # Let ActiveSupport::ProxyObject at least raise exceptions. def raise(*args) ::Object.send(:raise, *args) end diff --git a/activesupport/lib/active_support/queueing.rb b/activesupport/lib/active_support/queueing.rb deleted file mode 100644 index a89a48d057..0000000000 --- a/activesupport/lib/active_support/queueing.rb +++ /dev/null @@ -1,105 +0,0 @@ -require 'delegate' -require 'thread' - -module ActiveSupport - # A Queue that simply inherits from STDLIB's Queue. When this - # queue is used, Rails automatically starts a job runner in a - # background thread. - class Queue < ::Queue - attr_writer :consumer - - def initialize(consumer_options = {}) - super() - @consumer_options = consumer_options - end - - def consumer - @consumer ||= ThreadedQueueConsumer.new(self, @consumer_options) - end - - # Drain the queue, running all jobs in a different thread. This method - # may not be available on production queues. - def drain - # run the jobs in a separate thread so assumptions of synchronous - # jobs are caught in test mode. - consumer.drain - end - end - - class SynchronousQueue < Queue - def push(job) - super.tap { drain } - end - alias << push - alias enq push - end - - # In test mode, the Rails queue is backed by an Array so that assertions - # can be made about its contents. The test queue provides a +jobs+ - # method to make assertions about the queue's contents and a +drain+ - # method to drain the queue and run the jobs. - # - # Jobs are run in a separate thread to catch mistakes where code - # assumes that the job is run in the same thread. - class TestQueue < Queue - # Get a list of the jobs off this queue. This method may not be - # available on production queues. - def jobs - @que.dup - end - - # Marshal and unmarshal job before pushing it onto the queue. This will - # raise an exception on any attempts in tests to push jobs that can't (or - # shouldn't) be marshalled. - def push(job) - super Marshal.load(Marshal.dump(job)) - end - end - - # The threaded consumer will run jobs in a background thread in - # development mode or in a VM where running jobs on a thread in - # production mode makes sense. - # - # When the process exits, the consumer pushes a nil onto the - # queue and joins the thread, which will ensure that all jobs - # are executed before the process finally dies. - class ThreadedQueueConsumer - attr_accessor :logger - - def initialize(queue, options = {}) - @queue = queue - @logger = options[:logger] - @fallback_logger = Logger.new($stderr) - end - - def start - @thread = Thread.new { consume } - self - end - - def shutdown - @queue.push nil - @thread.join - end - - def drain - @queue.pop.run until @queue.empty? - end - - def consume - while job = @queue.pop - run job - end - end - - def run(job) - job.run - rescue Exception => exception - handle_exception job, exception - end - - def handle_exception(job, exception) - (logger || @fallback_logger).error "Job Error: #{job.inspect}\n#{exception.message}\n#{exception.backtrace.join("\n")}" - end - end -end diff --git a/activesupport/lib/active_support/rescuable.rb b/activesupport/lib/active_support/rescuable.rb index 9a038dfbca..a7eba91ac5 100644 --- a/activesupport/lib/active_support/rescuable.rb +++ b/activesupport/lib/active_support/rescuable.rb @@ -1,6 +1,5 @@ require 'active_support/concern' require 'active_support/core_ext/class/attribute' -require 'active_support/core_ext/proc' require 'active_support/core_ext/string/inflections' require 'active_support/core_ext/array/extract_options' diff --git a/activesupport/lib/active_support/subscriber.rb b/activesupport/lib/active_support/subscriber.rb new file mode 100644 index 0000000000..f2966f23fc --- /dev/null +++ b/activesupport/lib/active_support/subscriber.rb @@ -0,0 +1,116 @@ +require 'active_support/per_thread_registry' + +module ActiveSupport + # ActiveSupport::Subscriber is an object set to consume + # ActiveSupport::Notifications. The subscriber dispatches notifications to + # a registered object based on its given namespace. + # + # An example would be Active Record subscriber responsible for collecting + # statistics about queries: + # + # module ActiveRecord + # class StatsSubscriber < ActiveSupport::Subscriber + # def sql(event) + # Statsd.timing("sql.#{event.payload[:name]}", event.duration) + # end + # end + # end + # + # And it's finally registered as: + # + # ActiveRecord::StatsSubscriber.attach_to :active_record + # + # Since we need to know all instance methods before attaching the log + # subscriber, the line above should be called after your subscriber definition. + # + # After configured, whenever a "sql.active_record" notification is published, + # it will properly dispatch the event (ActiveSupport::Notifications::Event) to + # the +sql+ method. + class Subscriber + class << self + + # Attach the subscriber to a namespace. + def attach_to(namespace, subscriber=new, notifier=ActiveSupport::Notifications) + @namespace = namespace + @subscriber = subscriber + @notifier = notifier + + subscribers << subscriber + + # Add event subscribers for all existing methods on the class. + subscriber.public_methods(false).each do |event| + add_event_subscriber(event) + end + end + + # Adds event subscribers for all new methods added to the class. + def method_added(event) + # Only public methods are added as subscribers, and only if a notifier + # has been set up. This means that subscribers will only be set up for + # classes that call #attach_to. + if public_method_defined?(event) && notifier + add_event_subscriber(event) + end + end + + def subscribers + @@subscribers ||= [] + end + + protected + + attr_reader :subscriber, :notifier, :namespace + + def add_event_subscriber(event) + return if %w{ start finish }.include?(event.to_s) + + notifier.subscribe("#{event}.#{namespace}", subscriber) + end + end + + def initialize + @queue_key = [self.class.name, object_id].join "-" + super + end + + def start(name, id, payload) + e = ActiveSupport::Notifications::Event.new(name, Time.now, nil, id, payload) + parent = event_stack.last + parent << e if parent + + event_stack.push e + end + + def finish(name, id, payload) + finished = Time.now + event = event_stack.pop + event.end = finished + event.payload.merge!(payload) + + method = name.split('.').first + send(method, event) + end + + private + + def event_stack + SubscriberQueueRegistry.get_queue(@queue_key) + end + end + + # This is a registry for all the event stacks kept for subscribers. + # + # See the documentation of <tt>ActiveSupport::PerThreadRegistry</tt> + # for further details. + class SubscriberQueueRegistry # :nodoc: + extend PerThreadRegistry + + def initialize + @registry = {} + end + + def get_queue(queue_key) + @registry[queue_key] ||= [] + end + end +end diff --git a/activesupport/lib/active_support/test_case.rb b/activesupport/lib/active_support/test_case.rb index e4f182a3aa..2f27aff2bd 100644 --- a/activesupport/lib/active_support/test_case.rb +++ b/activesupport/lib/active_support/test_case.rb @@ -1,10 +1,10 @@ gem 'minitest' # make sure we get the gem, not stdlib -require 'minitest/spec' +require 'minitest' require 'active_support/testing/tagged_logging' require 'active_support/testing/setup_and_teardown' require 'active_support/testing/assertions' require 'active_support/testing/deprecation' -require 'active_support/testing/pending' +require 'active_support/testing/declarative' require 'active_support/testing/isolation' require 'active_support/testing/constant_lookup' require 'active_support/core_ext/kernel/reporting' @@ -15,16 +15,28 @@ begin rescue LoadError end -module ActiveSupport - class TestCase < ::MiniTest::Spec +module Minitest # :nodoc: + class << self + remove_method :__run + end - # Use AS::TestCase for the base class when describing a model - register_spec_type(self) do |desc| - Class === desc && desc < ActiveRecord::Base - end + def self.__run reporter, options # :nodoc: + # FIXME: MT5's runnables is not ordered. This is needed because + # we have have tests have cross-class order-dependent bugs. + suites = Runnable.runnables.sort_by { |ts| ts.name.to_s } + + parallel, serial = suites.partition { |s| s.test_order == :parallel } + + ParallelEach.new(parallel).map { |suite| suite.run reporter, options } + + serial.map { |suite| suite.run reporter, options } + end +end + +module ActiveSupport + class TestCase < ::Minitest::Test + Assertion = Minitest::Assertion - Assertion = MiniTest::Assertion - alias_method :method_name, :__name__ + alias_method :method_name, :name $tags = {} def self.for_tag(tag) @@ -41,32 +53,22 @@ module ActiveSupport include ActiveSupport::Testing::SetupAndTeardown include ActiveSupport::Testing::Assertions include ActiveSupport::Testing::Deprecation - include ActiveSupport::Testing::Pending - - def self.describe(text) - if block_given? - super - else - message = "`describe` without a block is deprecated, please switch to: `def self.name; #{text.inspect}; end`\n" - ActiveSupport::Deprecation.warn message - - class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1 - def self.name - "#{text}" - end - RUBY_EVAL - end - end - - class << self - alias :test :it - end + extend ActiveSupport::Testing::Declarative # test/unit backwards compatibility methods alias :assert_raise :assert_raises - alias :assert_not_nil :refute_nil + alias :assert_not_empty :refute_empty alias :assert_not_equal :refute_equal + alias :assert_not_in_delta :refute_in_delta + alias :assert_not_in_epsilon :refute_in_epsilon + alias :assert_not_includes :refute_includes + alias :assert_not_instance_of :refute_instance_of + alias :assert_not_kind_of :refute_kind_of alias :assert_no_match :refute_match + alias :assert_not_nil :refute_nil + alias :assert_not_operator :refute_operator + alias :assert_not_predicate :refute_predicate + alias :assert_not_respond_to :refute_respond_to alias :assert_not_same :refute_same # Fails if the block raises an exception. diff --git a/activesupport/lib/active_support/testing/assertions.rb b/activesupport/lib/active_support/testing/assertions.rb index 8466049e20..76a591bc3b 100644 --- a/activesupport/lib/active_support/testing/assertions.rb +++ b/activesupport/lib/active_support/testing/assertions.rb @@ -3,6 +3,22 @@ require 'active_support/core_ext/object/blank' module ActiveSupport module Testing module Assertions + # Assert that an expression is not truthy. Passes if <tt>object</tt> is + # +nil+ or +false+. "Truthy" means "considered true in a conditional" + # like <tt>if foo</tt>. + # + # assert_not nil # => true + # assert_not false # => true + # assert_not 'foo' # => 'foo' is not nil or false + # + # An error message can be specified. + # + # assert_not foo, 'foo should be false' + def assert_not(object, message = nil) + message ||= "Expected #{mu_pp(object)} to be nil or false" + assert !object, message + end + # Test numeric difference between the return value of an expression as a # result of what is evaluated in the yielded block. # @@ -76,34 +92,6 @@ module ActiveSupport def assert_no_difference(expression, message = nil, &block) assert_difference expression, 0, message, &block end - - # Test if an expression is blank. Passes if <tt>object.blank?</tt> - # is +true+. - # - # assert_blank [] # => true - # assert_blank [[]] # => [[]] is not blank - # - # An error message can be specified. - # - # assert_blank [], 'this should be blank' - def assert_blank(object, message=nil) - message ||= "#{object.inspect} is not blank" - assert object.blank?, message - end - - # Test if an expression is not blank. Passes if <tt>object.present?</tt> - # is +true+. - # - # assert_present({ data: 'x' }) # => true - # assert_present({}) # => {} is blank - # - # An error message can be specified. - # - # assert_present({ data: 'x' }, 'this should not be blank') - def assert_present(object, message=nil) - message ||= "#{object.inspect} is blank" - assert object.present?, message - end end end end diff --git a/activesupport/lib/active_support/testing/autorun.rb b/activesupport/lib/active_support/testing/autorun.rb new file mode 100644 index 0000000000..5aa5f46310 --- /dev/null +++ b/activesupport/lib/active_support/testing/autorun.rb @@ -0,0 +1,5 @@ +gem 'minitest' + +require 'minitest' + +Minitest.autorun diff --git a/activesupport/lib/active_support/testing/constant_lookup.rb b/activesupport/lib/active_support/testing/constant_lookup.rb index 52bfeb7179..1b2a75c35d 100644 --- a/activesupport/lib/active_support/testing/constant_lookup.rb +++ b/activesupport/lib/active_support/testing/constant_lookup.rb @@ -38,6 +38,8 @@ module ActiveSupport begin constant = names.join("::").constantize break(constant) if yield(constant) + rescue NoMethodError # subclass of NameError + raise rescue NameError # Constant wasn't found, move on ensure diff --git a/activesupport/lib/active_support/testing/declarative.rb b/activesupport/lib/active_support/testing/declarative.rb new file mode 100644 index 0000000000..1fa73caefa --- /dev/null +++ b/activesupport/lib/active_support/testing/declarative.rb @@ -0,0 +1,43 @@ +module ActiveSupport + module Testing + module Declarative + + def self.extended(klass) #:nodoc: + klass.class_eval do + + unless method_defined?(:describe) + def self.describe(text) + class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1 + def self.name + "#{text}" + end + RUBY_EVAL + end + end + + end + end + + unless defined?(Spec) + # Helper to define a test method using a String. Under the hood, it replaces + # spaces with underscores and defines the test method. + # + # test "verify something" do + # ... + # end + def test(name, &block) + test_name = "test_#{name.gsub(/\s+/,'_')}".to_sym + defined = instance_method(test_name) rescue false + raise "#{test_name} is already defined in #{self}" if defined + if block_given? + define_method(test_name, &block) + else + define_method(test_name) do + flunk "No implementation provided for #{name}" + end + end + end + end + end + end +end diff --git a/activesupport/lib/active_support/testing/isolation.rb b/activesupport/lib/active_support/testing/isolation.rb index 27d444fd91..d5d31cecbe 100644 --- a/activesupport/lib/active_support/testing/isolation.rb +++ b/activesupport/lib/active_support/testing/isolation.rb @@ -1,69 +1,11 @@ require 'rbconfig' +require 'minitest/parallel_each' + module ActiveSupport module Testing - class RemoteError < StandardError - - attr_reader :message, :backtrace - - def initialize(exception) - @message = "caught #{exception.class.name}: #{exception.message}" - @backtrace = exception.backtrace - end - end - - class ProxyTestResult - def initialize - @calls = [] - end - - def add_error(e) - e = Test::Unit::Error.new(e.test_name, RemoteError.new(e.exception)) - @calls << [:add_error, e] - end - - def __replay__(result) - @calls.each do |name, args| - result.send(name, *args) - end - end - - def method_missing(name, *args) - @calls << [name, args] - end - end - module Isolation require 'thread' - class ParallelEach - include Enumerable - - # default to 2 cores - CORES = (ENV['TEST_CORES'] || 2).to_i - - def initialize list - @list = list - @queue = SizedQueue.new CORES - end - - def grep pattern - self.class.new super - end - - def each - threads = CORES.times.map { - Thread.new { - while job = @queue.pop - yield job - end - } - } - @list.each { |i| @queue << i } - CORES.times { @queue << nil } - threads.each(&:join) - end - end - def self.included(klass) #:nodoc: klass.extend(Module.new { def test_methods @@ -76,23 +18,23 @@ module ActiveSupport !ENV["NO_FORK"] && ((RbConfig::CONFIG['host_os'] !~ /mswin|mingw/) && (RUBY_PLATFORM !~ /java/)) end + @@class_setup_mutex = Mutex.new + def _run_class_setup # class setup method should only happen in parent - unless defined?(@@ran_class_setup) || ENV['ISOLATION_TEST'] - self.class.setup if self.class.respond_to?(:setup) - @@ran_class_setup = true + @@class_setup_mutex.synchronize do + unless defined?(@@ran_class_setup) || ENV['ISOLATION_TEST'] + self.class.setup if self.class.respond_to?(:setup) + @@ran_class_setup = true + end end end - def run(runner) - _run_class_setup - - serialized = run_in_isolation do |isolated_runner| - super(isolated_runner) + def run + serialized = run_in_isolation do + super end - retval, proxy = Marshal.load(serialized) - proxy.__replay__(runner) - retval + Marshal.load(serialized) end module Forking @@ -101,9 +43,8 @@ module ActiveSupport pid = fork do read.close - proxy = ProxyTestResult.new - retval = yield proxy - write.puts [Marshal.dump([retval, proxy])].pack("m") + yield + write.puts [Marshal.dump(self.dup)].pack("m") exit! end @@ -123,19 +64,18 @@ module ActiveSupport require "tempfile" if ENV["ISOLATION_TEST"] - proxy = ProxyTestResult.new - retval = yield proxy + yield File.open(ENV["ISOLATION_OUTPUT"], "w") do |file| - file.puts [Marshal.dump([retval, proxy])].pack("m") + file.puts [Marshal.dump(self.dup)].pack("m") end exit! else Tempfile.open("isolation") do |tmpfile| - ENV["ISOLATION_TEST"] = @method_name + ENV["ISOLATION_TEST"] = self.class.name ENV["ISOLATION_OUTPUT"] = tmpfile.path load_paths = $-I.map {|p| "-I\"#{File.expand_path(p)}\"" }.join(" ") - `#{Gem.ruby} #{load_paths} #{$0} #{ORIG_ARGV.join(" ")} -t\"#{self.class}\"` + `#{Gem.ruby} #{load_paths} #{$0} #{ORIG_ARGV.join(" ")}` ENV.delete("ISOLATION_TEST") ENV.delete("ISOLATION_OUTPUT") diff --git a/activesupport/lib/active_support/testing/pending.rb b/activesupport/lib/active_support/testing/pending.rb deleted file mode 100644 index b04bbbbaea..0000000000 --- a/activesupport/lib/active_support/testing/pending.rb +++ /dev/null @@ -1,14 +0,0 @@ -require 'active_support/deprecation' - -module ActiveSupport - module Testing - module Pending # :nodoc: - unless defined?(Spec) - def pending(description = "", &block) - ActiveSupport::Deprecation.warn("#pending is deprecated and will be removed in Rails 4.1, please use #skip instead.") - skip(description.blank? ? nil : description) - end - end - end - end -end diff --git a/activesupport/lib/active_support/testing/performance.rb b/activesupport/lib/active_support/testing/performance.rb deleted file mode 100644 index 7102ffe2ed..0000000000 --- a/activesupport/lib/active_support/testing/performance.rb +++ /dev/null @@ -1,271 +0,0 @@ -require 'fileutils' -require 'active_support/concern' -require 'active_support/core_ext/class/delegating_attributes' -require 'active_support/core_ext/string/inflections' -require 'active_support/core_ext/module/delegation' -require 'active_support/number_helper' - -module ActiveSupport - module Testing - module Performance - extend ActiveSupport::Concern - - included do - superclass_delegating_accessor :profile_options - self.profile_options = {} - end - - # each implementation should define metrics and freeze the defaults - DEFAULTS = - if ARGV.include?('--benchmark') # HAX for rake test - { :runs => 4, - :output => 'tmp/performance', - :benchmark => true } - else - { :runs => 1, - :output => 'tmp/performance', - :benchmark => false } - end - - def full_profile_options - DEFAULTS.merge(profile_options) - end - - def full_test_name - "#{self.class.name}##{method_name}" - end - - def run(runner) - @runner = runner - - run_warmup - if full_profile_options && metrics = full_profile_options[:metrics] - metrics.each do |metric_name| - if klass = Metrics[metric_name.to_sym] - run_profile(klass.new) - end - end - end - - return - end - - def run_test(metric, mode) - result = '.' - begin - run_callbacks :setup - setup - metric.send(mode) { __send__ method_name } - rescue Exception => e - result = @runner.puke(self.class, method_name, e) - ensure - begin - teardown - run_callbacks :teardown - rescue Exception => e - result = @runner.puke(self.class, method_name, e) - end - end - result - end - - protected - # overridden by each implementation. - def run_gc; end - - def run_warmup - run_gc - - time = Metrics::Time.new - run_test(time, :benchmark) - puts "%s (%s warmup)" % [full_test_name, time.format(time.total)] - - run_gc - end - - def run_profile(metric) - klass = full_profile_options[:benchmark] ? Benchmarker : Profiler - performer = klass.new(self, metric) - - performer.run - puts performer.report - performer.record - end - - class Performer - delegate :run_test, :full_profile_options, :full_test_name, :to => :@harness - - def initialize(harness, metric) - @harness, @metric, @supported = harness, metric, false - end - - def report - if @supported - rate = @total / full_profile_options[:runs] - '%20s: %s' % [@metric.name, @metric.format(rate)] - else - '%20s: unsupported' % @metric.name - end - end - - protected - def output_filename - "#{full_profile_options[:output]}/#{full_test_name}_#{@metric.name}" - end - end - - # overridden by each implementation. - class Profiler < Performer - def time_with_block - before = Time.now - yield - Time.now - before - end - - def run; end - def record; end - end - - class Benchmarker < Performer - def initialize(*args) - super - @supported = @metric.respond_to?('measure') - end - - def run - return unless @supported - - full_profile_options[:runs].to_i.times { run_test(@metric, :benchmark) } - @total = @metric.total - end - - def record - avg = @metric.total / full_profile_options[:runs].to_i - now = Time.now.utc.xmlschema - with_output_file do |file| - file.puts "#{avg},#{now},#{environment}" - end - end - - def environment - @env ||= [].tap do |env| - env << "#{$1}.#{$2}" if File.directory?('.git') && `git branch -v` =~ /^\* (\S+)\s+(\S+)/ - env << rails_version if defined?(Rails::VERSION::STRING) - env << "#{RUBY_ENGINE}-#{RUBY_VERSION}.#{RUBY_PATCHLEVEL}" - env << RUBY_PLATFORM - end.join(',') - end - - protected - if defined?(Rails::VERSION::STRING) - HEADER = 'measurement,created_at,app,rails,ruby,platform' - else - HEADER = 'measurement,created_at,app,ruby,platform' - end - - def with_output_file - fname = output_filename - - if new = !File.exist?(fname) - FileUtils.mkdir_p(File.dirname(fname)) - end - - File.open(fname, 'ab') do |file| - file.puts(HEADER) if new - yield file - end - end - - def output_filename - "#{super}.csv" - end - - def rails_version - "rails-#{Rails::VERSION::STRING}#{rails_branch}" - end - - def rails_branch - if File.directory?('vendor/rails/.git') - Dir.chdir('vendor/rails') do - ".#{$1}.#{$2}" if `git branch -v` =~ /^\* (\S+)\s+(\S+)/ - end - end - end - end - - module Metrics - def self.[](name) - const_get(name.to_s.camelize) - rescue NameError - nil - end - - class Base - include ActiveSupport::NumberHelper - - attr_reader :total - - def initialize - @total = 0 - end - - def name - @name ||= self.class.name.demodulize.underscore - end - - def benchmark - with_gc_stats do - before = measure - yield - @total += (measure - before) - end - end - - # overridden by each implementation. - def profile; end - - protected - # overridden by each implementation. - def with_gc_stats; end - end - - class Time < Base - def measure - ::Time.now.to_f - end - - def format(measurement) - if measurement < 1 - '%d ms' % (measurement * 1000) - else - '%.2f sec' % measurement - end - end - end - - class Amount < Base - def format(measurement) - number_to_delimited(measurement.floor) - end - end - - class DigitalInformationUnit < Base - def format(measurement) - number_to_human_size(measurement, :precision => 2) - end - end - - # each implementation provides its own metrics like ProcessTime, Memory or GcRuns - end - end - end -end - -case RUBY_ENGINE - when 'ruby' then require 'active_support/testing/performance/ruby' - when 'rbx' then require 'active_support/testing/performance/rubinius' - when 'jruby' then require 'active_support/testing/performance/jruby' - else - $stderr.puts 'Your ruby interpreter is not supported for benchmarking.' - exit -end diff --git a/activesupport/lib/active_support/testing/performance/jruby.rb b/activesupport/lib/active_support/testing/performance/jruby.rb deleted file mode 100644 index 34e3f9f45f..0000000000 --- a/activesupport/lib/active_support/testing/performance/jruby.rb +++ /dev/null @@ -1,115 +0,0 @@ -require 'jruby/profiler' -require 'java' -java_import java.lang.management.ManagementFactory - -module ActiveSupport - module Testing - module Performance - DEFAULTS.merge!( - if ARGV.include?('--benchmark') - {:metrics => [:wall_time, :user_time, :memory, :gc_runs, :gc_time]} - else - { :metrics => [:wall_time], - :formats => [:flat, :graph] } - end).freeze - - protected - def run_gc - ManagementFactory.memory_mx_bean.gc - end - - class Profiler < Performer - def initialize(*args) - super - @supported = @metric.is_a?(Metrics::WallTime) - end - - def run - return unless @supported - - @total = time_with_block do - @data = JRuby::Profiler.profile do - full_profile_options[:runs].to_i.times { run_test(@metric, :profile) } - end - end - end - - def record - return unless @supported - - klasses = full_profile_options[:formats].map { |f| JRuby::Profiler.const_get("#{f.to_s.camelize}ProfilePrinter") }.compact - - klasses.each do |klass| - fname = output_filename(klass) - FileUtils.mkdir_p(File.dirname(fname)) - File.open(fname, 'wb') do |file| - klass.new(@data).printProfile(file) - end - end - end - - protected - def output_filename(printer_class) - suffix = - case printer_class.name.demodulize - when 'FlatProfilePrinter'; 'flat.txt' - when 'GraphProfilePrinter'; 'graph.txt' - else printer_class.name.sub(/ProfilePrinter$/, '').underscore - end - - "#{super()}_#{suffix}" - end - end - - module Metrics - class Base - def profile - yield - end - - protected - def with_gc_stats - ManagementFactory.memory_mx_bean.gc - yield - end - end - - class WallTime < Time - def measure - super - end - end - - class CpuTime < Time - def measure - ManagementFactory.thread_mx_bean.get_current_thread_cpu_time / 1000 / 1000 / 1000.0 # seconds - end - end - - class UserTime < Time - def measure - ManagementFactory.thread_mx_bean.get_current_thread_user_time / 1000 / 1000 / 1000.0 # seconds - end - end - - class Memory < DigitalInformationUnit - def measure - ManagementFactory.memory_mx_bean.non_heap_memory_usage.used + ManagementFactory.memory_mx_bean.heap_memory_usage.used - end - end - - class GcRuns < Amount - def measure - ManagementFactory.garbage_collector_mx_beans.inject(0) { |total_runs, current_gc| total_runs += current_gc.collection_count } - end - end - - class GcTime < Time - def measure - ManagementFactory.garbage_collector_mx_beans.inject(0) { |total_time, current_gc| total_time += current_gc.collection_time } / 1000.0 # seconds - end - end - end - end - end -end diff --git a/activesupport/lib/active_support/testing/performance/rubinius.rb b/activesupport/lib/active_support/testing/performance/rubinius.rb deleted file mode 100644 index d9ebfbe352..0000000000 --- a/activesupport/lib/active_support/testing/performance/rubinius.rb +++ /dev/null @@ -1,113 +0,0 @@ -require 'rubinius/agent' - -module ActiveSupport - module Testing - module Performance - DEFAULTS.merge!( - if ARGV.include?('--benchmark') - {:metrics => [:wall_time, :memory, :objects, :gc_runs, :gc_time]} - else - { :metrics => [:wall_time], - :formats => [:flat, :graph] } - end).freeze - - protected - def run_gc - GC.run(true) - end - - class Performer; end - - class Profiler < Performer - def initialize(*args) - super - @supported = @metric.is_a?(Metrics::WallTime) - end - - def run - return unless @supported - - @profiler = Rubinius::Profiler::Instrumenter.new - - @total = time_with_block do - @profiler.profile(false) do - full_profile_options[:runs].to_i.times { run_test(@metric, :profile) } - end - end - end - - def record - return unless @supported - - if(full_profile_options[:formats].include?(:flat)) - create_path_and_open_file(:flat) do |file| - @profiler.show(file) - end - end - - if(full_profile_options[:formats].include?(:graph)) - create_path_and_open_file(:graph) do |file| - @profiler.show(file) - end - end - end - - protected - def create_path_and_open_file(printer_name) - fname = "#{output_filename}_#{printer_name}.txt" - FileUtils.mkdir_p(File.dirname(fname)) - File.open(fname, 'wb') do |file| - yield(file) - end - end - end - - module Metrics - class Base - attr_reader :loopback - - def profile - yield - end - - protected - def with_gc_stats - @loopback = Rubinius::Agent.loopback - GC.run(true) - yield - end - end - - class WallTime < Time - def measure - super - end - end - - class Memory < DigitalInformationUnit - def measure - loopback.get("system.memory.counter.bytes").last - end - end - - class Objects < Amount - def measure - loopback.get("system.memory.counter.objects").last - end - end - - class GcRuns < Amount - def measure - loopback.get("system.gc.full.count").last + loopback.get("system.gc.young.count").last - end - end - - class GcTime < Time - def measure - (loopback.get("system.gc.full.wallclock").last + loopback.get("system.gc.young.wallclock").last) / 1000.0 - end - end - end - end - end -end diff --git a/activesupport/lib/active_support/testing/performance/ruby.rb b/activesupport/lib/active_support/testing/performance/ruby.rb deleted file mode 100644 index 7c149df1e4..0000000000 --- a/activesupport/lib/active_support/testing/performance/ruby.rb +++ /dev/null @@ -1,173 +0,0 @@ -begin - require 'ruby-prof' -rescue LoadError - $stderr.puts 'Specify ruby-prof as application\'s dependency in Gemfile to run benchmarks.' - raise -end - -module ActiveSupport - module Testing - module Performance - DEFAULTS.merge!( - if ARGV.include?('--benchmark') - { :metrics => [:wall_time, :memory, :objects, :gc_runs, :gc_time] } - else - { :min_percent => 0.01, - :metrics => [:process_time, :memory, :objects], - :formats => [:flat, :graph_html, :call_tree, :call_stack] } - end).freeze - - protected - remove_method :run_gc - def run_gc - GC.start - end - - class Profiler < Performer - def initialize(*args) - super - @supported = @metric.measure_mode rescue false - end - - remove_method :run - def run - return unless @supported - - RubyProf.measure_mode = @metric.measure_mode - RubyProf.start - RubyProf.pause - full_profile_options[:runs].to_i.times { run_test(@metric, :profile) } - @data = RubyProf.stop - @total = @data.threads.sum(0) { |thread| thread.methods.max.total_time } - end - - remove_method :record - def record - return unless @supported - - klasses = full_profile_options[:formats].map { |f| RubyProf.const_get("#{f.to_s.camelize}Printer") }.compact - - klasses.each do |klass| - fname = output_filename(klass) - FileUtils.mkdir_p(File.dirname(fname)) - File.open(fname, 'wb') do |file| - klass.new(@data).print(file, full_profile_options.slice(:min_percent)) - end - end - end - - protected - def output_filename(printer_class) - suffix = - case printer_class.name.demodulize - when 'FlatPrinter'; 'flat.txt' - when 'FlatPrinterWithLineNumbers'; 'flat_line_numbers.txt' - when 'GraphPrinter'; 'graph.txt' - when 'GraphHtmlPrinter'; 'graph.html' - when 'GraphYamlPrinter'; 'graph.yml' - when 'CallTreePrinter'; 'tree.txt' - when 'CallStackPrinter'; 'stack.html' - when 'DotPrinter'; 'graph.dot' - else printer_class.name.sub(/Printer$/, '').underscore - end - - "#{super()}_#{suffix}" - end - end - - module Metrics - class Base - def measure_mode - self.class::Mode - end - - remove_method :profile - def profile - RubyProf.resume - yield - ensure - RubyProf.pause - end - - protected - remove_method :with_gc_stats - def with_gc_stats - GC::Profiler.enable - GC.start - yield - ensure - GC::Profiler.disable - end - end - - class ProcessTime < Time - Mode = RubyProf::PROCESS_TIME if RubyProf.const_defined?(:PROCESS_TIME) - - def measure - RubyProf.measure_process_time - end - end - - class WallTime < Time - Mode = RubyProf::WALL_TIME if RubyProf.const_defined?(:WALL_TIME) - - def measure - RubyProf.measure_wall_time - end - end - - class CpuTime < Time - Mode = RubyProf::CPU_TIME if RubyProf.const_defined?(:CPU_TIME) - - def initialize(*args) - # FIXME: yeah my CPU is 2.33 GHz - RubyProf.cpu_frequency = 2.33e9 unless RubyProf.cpu_frequency > 0 - super - end - - def measure - RubyProf.measure_cpu_time - end - end - - class Memory < DigitalInformationUnit - Mode = RubyProf::MEMORY if RubyProf.const_defined?(:MEMORY) - - # Ruby 1.9 + GCdata patch - if GC.respond_to?(:malloc_allocated_size) - def measure - GC.malloc_allocated_size - end - end - end - - class Objects < Amount - Mode = RubyProf::ALLOCATIONS if RubyProf.const_defined?(:ALLOCATIONS) - - # Ruby 1.9 + GCdata patch - if GC.respond_to?(:malloc_allocations) - def measure - GC.malloc_allocations - end - end - end - - class GcRuns < Amount - Mode = RubyProf::GC_RUNS if RubyProf.const_defined?(:GC_RUNS) - - def measure - GC.count - end - end - - class GcTime < Time - Mode = RubyProf::GC_TIME if RubyProf.const_defined?(:GC_TIME) - - def measure - GC::Profiler.total_time - end - end - 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 a65148cf1f..33f2b8dc9b 100644 --- a/activesupport/lib/active_support/testing/setup_and_teardown.rb +++ b/activesupport/lib/active_support/testing/setup_and_teardown.rb @@ -3,6 +3,19 @@ require 'active_support/callbacks' module ActiveSupport module Testing + # Adds support for +setup+ and +teardown+ callbacks. + # These callbacks serve as a replacement to overwriting the + # <tt>#setup</tt> and <tt>#teardown</tt> methods of your TestCase. + # + # class ExampleTest < ActiveSupport::TestCase + # setup do + # # ... + # end + # + # teardown do + # # ... + # end + # end module SetupAndTeardown extend ActiveSupport::Concern @@ -12,21 +25,23 @@ module ActiveSupport end module ClassMethods + # Add a callback, which runs before <tt>TestCase#setup</tt>. def setup(*args, &block) set_callback(:setup, :before, *args, &block) end + # Add a callback, which runs after <tt>TestCase#teardown</tt>. def teardown(*args, &block) set_callback(:teardown, :after, *args, &block) end end - def before_setup + def before_setup # :nodoc: super run_callbacks :setup end - def after_teardown + def after_teardown # :nodoc: run_callbacks :teardown super end diff --git a/activesupport/lib/active_support/testing/tagged_logging.rb b/activesupport/lib/active_support/testing/tagged_logging.rb index 8ea2605733..f4cee64091 100644 --- a/activesupport/lib/active_support/testing/tagged_logging.rb +++ b/activesupport/lib/active_support/testing/tagged_logging.rb @@ -1,26 +1,25 @@ module ActiveSupport module Testing - module TaggedLogging + # Logs a "PostsControllerTest: test name" heading before each test to + # make test.log easier to search and follow along with. + module TaggedLogging #:nodoc: attr_writer :tagged_logger def before_setup - tagged_logger.push_tags(self.class.name, __name__) if tagged_logging? - super - end - - def after_teardown + if tagged_logger + heading = "#{self.class}: #{name}" + divider = '-' * heading.size + tagged_logger.info divider + tagged_logger.info heading + tagged_logger.info divider + end super - tagged_logger.pop_tags(2) if tagged_logging? end private def tagged_logger @tagged_logger ||= (defined?(Rails.logger) && Rails.logger) end - - def tagged_logging? - tagged_logger && tagged_logger.respond_to?(:push_tags) - end end end end diff --git a/activesupport/lib/active_support/time.rb b/activesupport/lib/active_support/time.rb index bcd5d78b54..92a593965e 100644 --- a/activesupport/lib/active_support/time.rb +++ b/activesupport/lib/active_support/time.rb @@ -9,21 +9,12 @@ end require 'date' require 'time' -require 'active_support/core_ext/time/marshal' -require 'active_support/core_ext/time/acts_like' -require 'active_support/core_ext/time/calculations' -require 'active_support/core_ext/time/conversions' -require 'active_support/core_ext/time/zones' - -require 'active_support/core_ext/date/acts_like' -require 'active_support/core_ext/date/calculations' -require 'active_support/core_ext/date/conversions' -require 'active_support/core_ext/date/zones' - -require 'active_support/core_ext/date_time/acts_like' -require 'active_support/core_ext/date_time/calculations' -require 'active_support/core_ext/date_time/conversions' -require 'active_support/core_ext/date_time/zones' +require 'active_support/core_ext/time' +require 'active_support/core_ext/date' +require 'active_support/core_ext/date_time' require 'active_support/core_ext/integer/time' require 'active_support/core_ext/numeric/time' + +require 'active_support/core_ext/string/conversions' +require 'active_support/core_ext/string/zones' diff --git a/activesupport/lib/active_support/time_with_zone.rb b/activesupport/lib/active_support/time_with_zone.rb index b931de3fac..95b9b8e5ae 100644 --- a/activesupport/lib/active_support/time_with_zone.rb +++ b/activesupport/lib/active_support/time_with_zone.rb @@ -80,22 +80,43 @@ module ActiveSupport end alias_method :getlocal, :localtime + # Returns true if the current time is within Daylight Savings Time for the + # specified time zone. + # + # Time.zone = 'Eastern Time (US & Canada)' # => 'Eastern Time (US & Canada)' + # Time.zone.parse("2012-5-30").dst? # => true + # Time.zone.parse("2012-11-30").dst? # => false def dst? period.dst? end alias_method :isdst, :dst? + # Returns true if the current time zone is set to UTC. + # + # Time.zone = 'UTC' # => 'UTC' + # Time.zone.now.utc? # => true + # Time.zone = 'Eastern Time (US & Canada)' # => 'Eastern Time (US & Canada)' + # Time.zone.now.utc? # => false def utc? time_zone.name == 'UTC' end alias_method :gmt?, :utc? + # Returns the offset from current time to UTC time in seconds. def utc_offset period.utc_total_offset end alias_method :gmt_offset, :utc_offset alias_method :gmtoff, :utc_offset + # Returns a formatted string of the offset from UTC, or an alternative + # string if the time zone is already UTC. + # + # Time.zone = 'Eastern Time (US & Canada)' # => "Eastern Time (US & Canada)" + # Time.zone.now.formatted_offset(true) # => "-05:00" + # Time.zone.now.formatted_offset(false) # => "-0500" + # Time.zone = 'UTC' # => "UTC" + # Time.zone.now.formatted_offset(true, "0") # => "0" def formatted_offset(colon = true, alternate_utc_string = nil) utc? && alternate_utc_string || TimeZone.seconds_to_utc_offset(utc_offset, colon) end @@ -133,7 +154,7 @@ module ActiveSupport # # => "2005/02/01 15:15:10 +0000" def as_json(options = nil) if ActiveSupport::JSON::Encoding.use_standard_json_time_format - xmlschema + xmlschema(3) else %(#{time.strftime("%Y/%m/%d %H:%M:%S")} #{formatted_offset(false)}) end @@ -147,10 +168,18 @@ module ActiveSupport end end + # Returns a string of the object's date and time in the format used by + # HTTP requests. + # + # Time.zone.now.httpdate # => "Tue, 01 Jan 2013 04:39:43 GMT" def httpdate utc.httpdate end + # Returns a string of the object's date and time in the RFC 2822 standard + # format. + # + # Time.zone.now.rfc2822 # => "Tue, 01 Jan 2013 04:51:39 +0000" def rfc2822 to_s(:rfc822) end @@ -185,18 +214,24 @@ module ActiveSupport utc <=> other end + # Returns true if the current object's time is within the specified + # +min+ and +max+ time. def between?(min, max) utc.between?(min, max) end + # Returns true if the current object's time is in the past. def past? utc.past? end + # Returns true if the current object's time falls within + # the current day. def today? time.today? end + # Returns true if the current object's time is in the future. def future? utc.future? end @@ -257,7 +292,7 @@ module ActiveSupport end end - %w(year mon month day mday wday yday hour min sec to_date).each do |method_name| + %w(year mon month day mday wday yday hour min sec usec nsec to_date).each do |method_name| class_eval <<-EOV, __FILE__, __LINE__ + 1 def #{method_name} # def month time.#{method_name} # time.month @@ -265,10 +300,6 @@ module ActiveSupport EOV end - def usec - time.respond_to?(:usec) ? time.usec : 0 - end - def to_a [time.sec, time.min, time.hour, time.day, time.mon, time.year, time.wday, time.yday, dst?, zone] end @@ -282,9 +313,13 @@ module ActiveSupport end alias_method :tv_sec, :to_i - # A TimeWithZone acts like a Time, so just return +self+. + def to_r + utc.to_r + end + + # Return an instance of Time in the system timezone. def to_time - utc + utc.to_time end def to_datetime @@ -327,6 +362,8 @@ module ActiveSupport # TimeWithZone with the existing +time_zone+. def method_missing(sym, *args, &block) wrap_with_time_zone time.__send__(sym, *args, &block) + rescue NoMethodError => e + raise e, e.message.sub(time.inspect, self.inspect), e.backtrace end private @@ -344,7 +381,7 @@ module ActiveSupport end def transfer_time_values_to_utc_constructor(time) - ::Time.utc_time(time.year, time.month, time.day, time.hour, time.min, time.sec, time.respond_to?(:nsec) ? Rational(time.nsec, 1000) : 0) + ::Time.utc(time.year, time.month, time.day, time.hour, time.min, time.sec, Rational(time.nsec, 1000)) end def duration_of_variable_length?(obj) diff --git a/activesupport/lib/active_support/values/time_zone.rb b/activesupport/lib/active_support/values/time_zone.rb index d087955587..3cf82a24b9 100644 --- a/activesupport/lib/active_support/values/time_zone.rb +++ b/activesupport/lib/active_support/values/time_zone.rb @@ -5,7 +5,7 @@ module ActiveSupport # The TimeZone class serves as a wrapper around TZInfo::Timezone instances. # It allows us to do the following: # - # * Limit the set of zones provided by TZInfo to a meaningful subset of 142 + # * Limit the set of zones provided by TZInfo to a meaningful subset of 146 # zones. # * Retrieve and display zones with a friendlier name # (e.g., "Eastern Time (US & Canada)" instead of "America/New_York"). @@ -62,6 +62,7 @@ module ActiveSupport "Newfoundland" => "America/St_Johns", "Brasilia" => "America/Sao_Paulo", "Buenos Aires" => "America/Argentina/Buenos_Aires", + "Montevideo" => "America/Montevideo", "Georgetown" => "America/Guyana", "Greenland" => "America/Godthab", "Mid-Atlantic" => "Atlantic/South_Georgia", @@ -150,7 +151,7 @@ module ActiveSupport "Taipei" => "Asia/Taipei", "Perth" => "Australia/Perth", "Irkutsk" => "Asia/Irkutsk", - "Ulaan Bataar" => "Asia/Ulaanbaatar", + "Ulaanbaatar" => "Asia/Ulaanbaatar", "Seoul" => "Asia/Seoul", "Osaka" => "Asia/Tokyo", "Sapporo" => "Asia/Tokyo", @@ -176,6 +177,7 @@ module ActiveSupport "Wellington" => "Pacific/Auckland", "Nuku'alofa" => "Pacific/Tongatapu", "Tokelau Is." => "Pacific/Fakaofo", + "Chatham Is." => "Pacific/Chatham", "Samoa" => "Pacific/Apia" } @@ -238,7 +240,7 @@ module ActiveSupport # Compare #name and TZInfo identifier to a supplied regexp, returning +true+ # if a match is found. def =~(re) - return true if name =~ re || MAPPING[name] =~ re + re === name || re === MAPPING[name] end # Returns a textual representation of this time zone. @@ -252,7 +254,7 @@ module ActiveSupport # Time.zone = 'Hawaii' # => "Hawaii" # Time.zone.local(2007, 2, 1, 15, 30, 45) # => Thu, 01 Feb 2007 15:30:45 HST -10:00 def local(*args) - time = Time.utc_time(*args) + time = Time.utc(*args) ActiveSupport::TimeWithZone.new(nil, self, time) end diff --git a/activesupport/lib/active_support/values/unicode_tables.dat b/activesupport/lib/active_support/values/unicode_tables.dat Binary files differindex df17a8cccf..2571faa019 100644 --- a/activesupport/lib/active_support/values/unicode_tables.dat +++ b/activesupport/lib/active_support/values/unicode_tables.dat diff --git a/activesupport/lib/active_support/version.rb b/activesupport/lib/active_support/version.rb index 8a8f8f946d..8762330a6e 100644 --- a/activesupport/lib/active_support/version.rb +++ b/activesupport/lib/active_support/version.rb @@ -1,10 +1,11 @@ module ActiveSupport - module VERSION #:nodoc: - MAJOR = 4 - MINOR = 0 - TINY = 0 - PRE = "beta" + # Returns the version of the currently loaded ActiveSupport as a Gem::Version + def self.version + Gem::Version.new "4.1.0.beta" + end - STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.') + module VERSION #:nodoc: + MAJOR, MINOR, TINY, PRE = ActiveSupport.version.segments + STRING = ActiveSupport.version.to_s end end diff --git a/activesupport/lib/active_support/xml_mini/jdom.rb b/activesupport/lib/active_support/xml_mini/jdom.rb index 4551dd2f2d..27c64c4dca 100644 --- a/activesupport/lib/active_support/xml_mini/jdom.rb +++ b/activesupport/lib/active_support/xml_mini/jdom.rb @@ -37,6 +37,12 @@ module ActiveSupport {} else @dbf = DocumentBuilderFactory.new_instance + # secure processing of java xml + # http://www.ibm.com/developerworks/xml/library/x-tipcfsx/index.html + @dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false) + @dbf.setFeature("http://xml.org/sax/features/external-general-entities", false) + @dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false) + @dbf.setFeature(javax.xml.XMLConstants::FEATURE_SECURE_PROCESSING, true) xml_string_reader = StringReader.new(data) xml_input_source = InputSource.new(xml_string_reader) doc = @dbf.new_document_builder.parse(xml_input_source) diff --git a/activesupport/lib/active_support/xml_mini/libxmlsax.rb b/activesupport/lib/active_support/xml_mini/libxmlsax.rb index acc018fd2d..70a95299ec 100644 --- a/activesupport/lib/active_support/xml_mini/libxmlsax.rb +++ b/activesupport/lib/active_support/xml_mini/libxmlsax.rb @@ -32,7 +32,7 @@ module ActiveSupport end def on_start_element(name, attrs = {}) - new_hash = { CONTENT_KEY => '' }.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/nokogirisax.rb b/activesupport/lib/active_support/xml_mini/nokogirisax.rb index 30b94aac47..be2d6a4cb1 100644 --- a/activesupport/lib/active_support/xml_mini/nokogirisax.rb +++ b/activesupport/lib/active_support/xml_mini/nokogirisax.rb @@ -38,7 +38,7 @@ module ActiveSupport end def start_element(name, attrs = []) - new_hash = { CONTENT_KEY => '' }.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/test/abstract_unit.rb b/activesupport/test/abstract_unit.rb index 8a67b148c3..65de48a7f6 100644 --- a/activesupport/test/abstract_unit.rb +++ b/activesupport/test/abstract_unit.rb @@ -8,18 +8,29 @@ ensure end require 'active_support/core_ext/kernel/reporting' -require 'active_support/core_ext/string/encoding' silence_warnings do Encoding.default_internal = "UTF-8" Encoding.default_external = "UTF-8" end -require 'minitest/autorun' +require 'active_support/testing/autorun' require 'empty_bool' ENV['NO_RELOAD'] = '1' require 'active_support' +Thread.abort_on_exception = true + # Show backtraces for deprecated behavior for quicker cleanup. ActiveSupport::Deprecation.debug = true + +# 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 RUBY_ENGINE == 'jruby' +end diff --git a/activesupport/test/autoloading_fixtures/html/some_class.rb b/activesupport/test/autoloading_fixtures/html/some_class.rb new file mode 100644 index 0000000000..b43d15d891 --- /dev/null +++ b/activesupport/test/autoloading_fixtures/html/some_class.rb @@ -0,0 +1,4 @@ +module HTML + class SomeClass + end +end diff --git a/activesupport/test/caching_test.rb b/activesupport/test/caching_test.rb index a2e2c51283..16d087ab9d 100644 --- a/activesupport/test/caching_test.rb +++ b/activesupport/test/caching_test.rb @@ -1,7 +1,7 @@ require 'logger' require 'abstract_unit' require 'active_support/cache' -require 'dependecies_test_helpers' +require 'dependencies_test_helpers' class CacheKeyTest < ActiveSupport::TestCase def test_entry_legacy_optional_ivars @@ -257,6 +257,26 @@ module CacheStoreBehavior assert_equal({"fu" => "baz"}, @cache.read_multi('foo', 'fu')) end + def test_fetch_multi + @cache.write('foo', 'bar') + @cache.write('fud', 'biz') + + values = @cache.fetch_multi('foo', 'fu', 'fud') {|value| value * 2 } + + assert_equal(["bar", "fufu", "biz"], values) + assert_equal("fufu", @cache.read('fu')) + end + + def test_multi_with_objects + foo = stub(:title => "FOO!", :cache_key => "foo") + bar = stub(:cache_key => "bar") + + @cache.write('bar', "BAM!") + + values = @cache.fetch_multi(foo, bar) {|object| object.title } + assert_equal(["FOO!", "BAM!"], values) + end + def test_read_and_write_compressed_small_data @cache.write('foo', 'bar', :compress => true) assert_equal 'bar', @cache.read('foo') @@ -307,8 +327,8 @@ module CacheStoreBehavior def test_exist @cache.write('foo', 'bar') - assert @cache.exist?('foo') - assert !@cache.exist?('bar') + assert_equal true, @cache.exist?('foo') + assert_equal false, @cache.exist?('bar') end def test_nil_exist @@ -405,7 +425,7 @@ module CacheStoreBehavior end # https://rails.lighthouseapp.com/projects/8994/tickets/6225-memcachestore-cant-deal-with-umlauts-and-special-characters -# The error is caused by charcter encodings that can't be compared with ASCII-8BIT regular expressions and by special +# The error is caused by character encodings that can't be compared with ASCII-8BIT regular expressions and by special # characters like the umlaut in UTF-8. module EncodedKeyCacheBehavior Encoding.list.each do |encoding| @@ -564,7 +584,7 @@ module LocalCacheBehavior end module AutoloadingCacheBehavior - include DependeciesTestHelpers + include DependenciesTestHelpers def test_simple_autoloading with_autoloading_fixtures do @cache.write('foo', E.new) @@ -672,17 +692,41 @@ class FileStoreTest < ActiveSupport::TestCase end end + def test_delete_does_not_delete_empty_parent_dir + sub_cache_dir = File.join(cache_dir, 'subdir/') + sub_cache_store = ActiveSupport::Cache::FileStore.new(sub_cache_dir) + assert_nothing_raised(Exception) do + assert sub_cache_store.write('foo', 'bar') + assert sub_cache_store.delete('foo') + end + assert File.exist?(cache_dir), "Parent of top level cache dir was deleted!" + assert File.exist?(sub_cache_dir), "Top level cache dir was deleted!" + assert Dir.entries(sub_cache_dir).reject {|f| ActiveSupport::Cache::FileStore::EXCLUDED_DIRS.include?(f)}.empty? + end + def test_log_exception_when_cache_read_fails File.expects(:exist?).raises(StandardError, "failed") @cache.send(:read_entry, "winston", {}) - assert_present @buffer.string + assert @buffer.string.present? + end + + def test_cleanup_removes_all_expired_entries + time = Time.now + @cache.write('foo', 'bar', expires_in: 10) + @cache.write('baz', 'qux') + @cache.write('quux', 'corge', expires_in: 20) + Time.stubs(:now).returns(time + 15) + @cache.cleanup + assert_not @cache.exist?('foo') + assert @cache.exist?('baz') + assert @cache.exist?('quux') end end class MemoryStoreTest < ActiveSupport::TestCase def setup - @record_size = ActiveSupport::Cache::Entry.new("aaaaaaaaaa").size - @cache = ActiveSupport::Cache.lookup_store(:memory_store, :expires_in => 60, :size => @record_size * 10) + @record_size = ActiveSupport::Cache.lookup_store(:memory_store).send(:cached_size, 1, ActiveSupport::Cache::Entry.new("aaaaaaaaaa")) + @cache = ActiveSupport::Cache.lookup_store(:memory_store, :expires_in => 60, :size => @record_size * 10 + 1) end include CacheStoreBehavior @@ -732,6 +776,30 @@ class MemoryStoreTest < ActiveSupport::TestCase assert !@cache.exist?(1), "no entry" end + def test_prune_size_on_write_based_on_key_length + @cache.write(1, "aaaaaaaaaa") && sleep(0.001) + @cache.write(2, "bbbbbbbbbb") && sleep(0.001) + @cache.write(3, "cccccccccc") && sleep(0.001) + @cache.write(4, "dddddddddd") && sleep(0.001) + @cache.write(5, "eeeeeeeeee") && sleep(0.001) + @cache.write(6, "ffffffffff") && sleep(0.001) + @cache.write(7, "gggggggggg") && sleep(0.001) + @cache.write(8, "hhhhhhhhhh") && sleep(0.001) + @cache.write(9, "iiiiiiiiii") && sleep(0.001) + long_key = '*' * 2 * @record_size + @cache.write(long_key, "llllllllll") + assert @cache.exist?(long_key) + assert @cache.exist?(9) + assert @cache.exist?(8) + assert @cache.exist?(7) + assert @cache.exist?(6) + assert !@cache.exist?(5), "no entry" + assert !@cache.exist?(4), "no entry" + assert !@cache.exist?(3), "no entry" + assert !@cache.exist?(2), "no entry" + assert !@cache.exist?(1), "no entry" + end + def test_pruning_is_capped_at_a_max_time def @cache.delete_entry (*args) sleep(0.01) @@ -897,12 +965,12 @@ class CacheStoreLoggerTest < ActiveSupport::TestCase def test_logging @cache.fetch('foo') { 'bar' } - assert_present @buffer.string + assert @buffer.string.present? end def test_mute_logging @cache.mute { @cache.fetch('foo') { 'bar' } } - assert_blank @buffer.string + assert @buffer.string.blank? end end @@ -931,29 +999,28 @@ class CacheEntryTest < ActiveSupport::TestCase assert_equal value.bytesize, entry.size end - def test_restoring_version_3_entries - version_3_entry = ActiveSupport::Cache::Entry.allocate - version_3_entry.instance_variable_set(:@value, "hello") - version_3_entry.instance_variable_set(:@created_at, Time.now - 60) - entry = Marshal.load(Marshal.dump(version_3_entry)) + def test_restoring_version_4beta1_entries + version_4beta1_entry = ActiveSupport::Cache::Entry.allocate + version_4beta1_entry.instance_variable_set(:@v, "hello") + version_4beta1_entry.instance_variable_set(:@x, Time.now.to_i + 60) + entry = Marshal.load(Marshal.dump(version_4beta1_entry)) assert_equal "hello", entry.value assert_equal false, entry.expired? end - def test_restoring_compressed_version_3_entries - version_3_entry = ActiveSupport::Cache::Entry.allocate - version_3_entry.instance_variable_set(:@value, Zlib::Deflate.deflate(Marshal.dump("hello"))) - version_3_entry.instance_variable_set(:@compressed, true) - entry = Marshal.load(Marshal.dump(version_3_entry)) + def test_restoring_compressed_version_4beta1_entries + version_4beta1_entry = ActiveSupport::Cache::Entry.allocate + version_4beta1_entry.instance_variable_set(:@v, Zlib::Deflate.deflate(Marshal.dump("hello"))) + version_4beta1_entry.instance_variable_set(:@c, true) + entry = Marshal.load(Marshal.dump(version_4beta1_entry)) assert_equal "hello", entry.value end - def test_restoring_expired_version_3_entries - version_3_entry = ActiveSupport::Cache::Entry.allocate - version_3_entry.instance_variable_set(:@value, "hello") - version_3_entry.instance_variable_set(:@created_at, Time.now - 60) - version_3_entry.instance_variable_set(:@expires_in, 58.9) - entry = Marshal.load(Marshal.dump(version_3_entry)) + def test_restoring_expired_version_4beta1_entries + version_4beta1_entry = ActiveSupport::Cache::Entry.allocate + version_4beta1_entry.instance_variable_set(:@v, "hello") + version_4beta1_entry.instance_variable_set(:@x, Time.now.to_i - 1) + entry = Marshal.load(Marshal.dump(version_4beta1_entry)) assert_equal "hello", entry.value assert_equal true, entry.expired? end diff --git a/activesupport/test/callback_inheritance_test.rb b/activesupport/test/callback_inheritance_test.rb index 6be8ea8b84..1adfe4edf4 100644 --- a/activesupport/test/callback_inheritance_test.rb +++ b/activesupport/test/callback_inheritance_test.rb @@ -109,7 +109,6 @@ class BasicCallbacksTest < ActiveSupport::TestCase @index = GrandParent.new("index").dispatch @update = GrandParent.new("update").dispatch @delete = GrandParent.new("delete").dispatch - @unknown = GrandParent.new("unknown").dispatch end def test_basic_conditional_callback1 @@ -130,7 +129,6 @@ class InheritedCallbacksTest < ActiveSupport::TestCase @index = Parent.new("index").dispatch @update = Parent.new("update").dispatch @delete = Parent.new("delete").dispatch - @unknown = Parent.new("unknown").dispatch end def test_inherited_excluded diff --git a/activesupport/test/callbacks_test.rb b/activesupport/test/callbacks_test.rb index 8810302f40..f8e2ce22fa 100644 --- a/activesupport/test/callbacks_test.rb +++ b/activesupport/test/callbacks_test.rb @@ -66,6 +66,16 @@ module CallbacksTest end end + class CallbackClass + def self.before(model) + model.history << [:before_save, :class] + end + + def self.after(model) + model.history << [:after_save, :class] + end + end + class Person < Record [:before_save, :after_save].each do |callback_method| callback_method_sym = callback_method.to_sym @@ -73,6 +83,7 @@ module CallbacksTest send(callback_method, callback_string(callback_method_sym)) send(callback_method, callback_proc(callback_method_sym)) send(callback_method, callback_object(callback_method_sym.to_s.gsub(/_save/, ''))) + send(callback_method, CallbackClass) send(callback_method) { |model| model.history << [callback_method_sym, :block] } end @@ -86,10 +97,14 @@ module CallbacksTest skip_callback :save, :after, :before_save_method, :unless => :yes skip_callback :save, :after, :before_save_method, :if => :no skip_callback :save, :before, :before_save_method, :unless => :no + skip_callback :save, :before, CallbackClass , :if => :yes def yes; true; end def no; false; end end + class PersonForProgrammaticSkipping < Person + end + class ParentController include ActiveSupport::Callbacks @@ -297,7 +312,7 @@ module CallbacksTest end end end - + class AroundPersonResult < MySuper attr_reader :result @@ -308,7 +323,7 @@ module CallbacksTest def tweedle_dum @result = yield end - + def tweedle_1 :tweedle_1 end @@ -316,7 +331,7 @@ module CallbacksTest def tweedle_2 :tweedle_2 end - + def save run_callbacks :save do :running @@ -410,7 +425,7 @@ module CallbacksTest ], around.history end end - + class AroundCallbackResultTest < ActiveSupport::TestCase def test_save_around around = AroundPersonResult.new @@ -430,6 +445,26 @@ module CallbacksTest [:before_save, :object], [:before_save, :block], [:after_save, :block], + [:after_save, :class], + [:after_save, :object], + [:after_save, :proc], + [:after_save, :string], + [:after_save, :symbol] + ], person.history + end + + def test_skip_person_programmatically + PersonForProgrammaticSkipping._save_callbacks.each do |save_callback| + if "before" == save_callback.kind.to_s + PersonForProgrammaticSkipping.skip_callback("save", save_callback.kind, save_callback.filter) + end + end + person = PersonForProgrammaticSkipping.new + assert_equal [], person.history + person.save + assert_equal [ + [:after_save, :block], + [:after_save, :class], [:after_save, :object], [:after_save, :proc], [:after_save, :string], @@ -449,8 +484,10 @@ module CallbacksTest [:before_save, :string], [:before_save, :proc], [:before_save, :object], + [:before_save, :class], [:before_save, :block], [:after_save, :block], + [:after_save, :class], [:after_save, :object], [:after_save, :proc], [:after_save, :string], @@ -488,7 +525,7 @@ module CallbacksTest class CallbackTerminator include ActiveSupport::Callbacks - define_callbacks :save, :terminator => "result == :halt" + define_callbacks :save, :terminator => ->(_,result) { result == :halt } set_callback :save, :before, :first set_callback :save, :before, :second @@ -607,6 +644,45 @@ module CallbacksTest end end + class OneTwoThreeSave + include ActiveSupport::Callbacks + + define_callbacks :save + + attr_accessor :record + + def initialize + @record = [] + end + + def save + run_callbacks :save do + @record << "yielded" + end + end + + def first + @record << "one" + end + + def second + @record << "two" + end + + def third + @record << "three" + end + end + + class DuplicatingCallbacks < OneTwoThreeSave + set_callback :save, :before, :first, :second + set_callback :save, :before, :first, :third + end + + class DuplicatingCallbacksInSameCall < OneTwoThreeSave + set_callback :save, :before, :first, :second, :first, :third + end + class UsingObjectTest < ActiveSupport::TestCase def test_before_object u = UsingObjectBefore.new @@ -642,7 +718,7 @@ module CallbacksTest def test_termination_invokes_hook terminator = CallbackTerminator.new terminator.save - assert_equal ":second", terminator.halted + assert_equal :second, terminator.halted end def test_block_never_called_if_terminated @@ -676,8 +752,10 @@ module CallbacksTest [:before_save, :string], [:before_save, :proc], [:before_save, :object], + [:before_save, :class], [:before_save, :block], [:after_save, :block], + [:after_save, :class], [:after_save, :object], [:after_save, :proc], [:after_save, :string], @@ -694,20 +772,253 @@ module CallbacksTest end end - class PerKeyOptionDeprecationTest < ActiveSupport::TestCase + class ExcludingDuplicatesCallbackTest < ActiveSupport::TestCase + def test_excludes_duplicates_in_separate_calls + model = DuplicatingCallbacks.new + model.save + assert_equal ["two", "one", "three", "yielded"], model.record + end - def test_per_key_option_deprecaton - assert_raise NotImplementedError do - Phone.class_eval do - set_callback :save, :before, :before_save1, :per_key => {:if => "true"} - end + def test_excludes_duplicates_in_one_call + model = DuplicatingCallbacksInSameCall.new + model.save + assert_equal ["two", "one", "three", "yielded"], model.record + end + end + + class CallbackProcTest < ActiveSupport::TestCase + def build_class(callback) + Class.new { + include ActiveSupport::Callbacks + define_callbacks :foo + set_callback :foo, :before, callback + def run; run_callbacks :foo; end + } + end + + def test_proc_arity_0 + calls = [] + klass = build_class(->() { calls << :foo }) + klass.new.run + assert_equal [:foo], calls + end + + def test_proc_arity_1 + calls = [] + klass = build_class(->(o) { calls << o }) + instance = klass.new + instance.run + assert_equal [instance], calls + end + + def test_proc_arity_2 + assert_raises(ArgumentError) do + klass = build_class(->(x,y) { }) + klass.new.run end - assert_raise NotImplementedError do - Phone.class_eval do - skip_callback :save, :before, :before_save1, :per_key => {:if => "true"} - end + end + + def test_proc_negative_called_with_empty_list + calls = [] + klass = build_class(->(*args) { calls << args }) + klass.new.run + assert_equal [[]], calls + end + end + + class ConditionalTests < ActiveSupport::TestCase + def build_class(callback) + Class.new { + include ActiveSupport::Callbacks + define_callbacks :foo + set_callback :foo, :before, :foo, :if => callback + def foo; end + def run; run_callbacks :foo; end + } + end + + # FIXME: do we really want to support classes as conditionals? There were + # no tests for it previous to this. + def test_class_conditional_with_scope + z = [] + callback = Class.new { + define_singleton_method(:foo) { |o| z << o } + } + klass = Class.new { + include ActiveSupport::Callbacks + define_callbacks :foo, :scope => [:name] + set_callback :foo, :before, :foo, :if => callback + def run; run_callbacks :foo; end + private + def foo; end + } + object = klass.new + object.run + assert_equal [object], z + end + + # FIXME: do we really want to support classes as conditionals? There were + # no tests for it previous to this. + def test_class + z = [] + klass = build_class Class.new { + define_singleton_method(:before) { |o| z << o } + } + object = klass.new + object.run + assert_equal [object], z + end + + def test_proc_negative_arity # passes an empty list if *args + z = [] + object = build_class(->(*args) { z << args }).new + object.run + assert_equal [], z.flatten + end + + def test_proc_arity0 + z = [] + object = build_class(->() { z << 0 }).new + object.run + assert_equal [0], z + end + + def test_proc_arity1 + z = [] + object = build_class(->(x) { z << x }).new + object.run + assert_equal [object], z + end + + def test_proc_arity2 + assert_raises(ArgumentError) do + object = build_class(->(a,b) { }).new + object.run end end end - + + class ResetCallbackTest < ActiveSupport::TestCase + def build_class(memo) + klass = Class.new { + include ActiveSupport::Callbacks + define_callbacks :foo + set_callback :foo, :before, :hello + def run; run_callbacks :foo; end + } + klass.class_eval { + define_method(:hello) { memo << :hi } + } + klass + end + + def test_reset_callbacks + events = [] + klass = build_class events + klass.new.run + assert_equal 1, events.length + + klass.reset_callbacks :foo + klass.new.run + assert_equal 1, events.length + end + + def test_reset_impacts_subclasses + events = [] + klass = build_class events + subclass = Class.new(klass) { set_callback :foo, :before, :world } + subclass.class_eval { define_method(:world) { events << :world } } + + subclass.new.run + assert_equal 2, events.length + + klass.reset_callbacks :foo + subclass.new.run + assert_equal 3, events.length + end + end + + class CallbackTypeTest < ActiveSupport::TestCase + def build_class(callback, n = 10) + Class.new { + include ActiveSupport::Callbacks + define_callbacks :foo + n.times { set_callback :foo, :before, callback } + def run; run_callbacks :foo; end + def self.skip(thing); skip_callback :foo, :before, thing; end + } + end + + def test_add_class + calls = [] + callback = Class.new { + define_singleton_method(:before) { |o| calls << o } + } + build_class(callback).new.run + assert_equal 10, calls.length + end + + def test_add_lambda + calls = [] + build_class(->(o) { calls << o }).new.run + assert_equal 10, calls.length + end + + def test_add_symbol + calls = [] + klass = build_class(:bar) + klass.class_eval { define_method(:bar) { calls << klass } } + klass.new.run + assert_equal 1, calls.length + end + + def test_add_eval + calls = [] + klass = build_class("bar") + klass.class_eval { define_method(:bar) { calls << klass } } + klass.new.run + assert_equal 1, calls.length + end + + def test_skip_class # removes one at a time + calls = [] + callback = Class.new { + define_singleton_method(:before) { |o| calls << o } + } + klass = build_class(callback) + 9.downto(0) { |i| + klass.skip callback + klass.new.run + assert_equal i, calls.length + calls.clear + } + end + + def test_skip_lambda # removes nothing + calls = [] + callback = ->(o) { calls << o } + klass = build_class(callback) + 10.times { klass.skip callback } + klass.new.run + assert_equal 10, calls.length + end + + def test_skip_symbol # removes all + calls = [] + klass = build_class(:bar) + klass.class_eval { define_method(:bar) { calls << klass } } + klass.skip :bar + klass.new.run + assert_equal 0, calls.length + end + + def test_skip_eval # removes nothing + calls = [] + klass = build_class("bar") + klass.class_eval { define_method(:bar) { calls << klass } } + klass.skip "bar" + klass.new.run + assert_equal 1, calls.length + end + end end diff --git a/activesupport/test/clean_backtrace_test.rb b/activesupport/test/clean_backtrace_test.rb index b14950acb3..dd67a45cf6 100644 --- a/activesupport/test/clean_backtrace_test.rb +++ b/activesupport/test/clean_backtrace_test.rb @@ -36,6 +36,27 @@ class BacktraceCleanerSilencerTest < ActiveSupport::TestCase end end +class BacktraceCleanerMultipleSilencersTest < ActiveSupport::TestCase + def setup + @bc = ActiveSupport::BacktraceCleaner.new + @bc.add_silencer { |line| line =~ /mongrel/ } + @bc.add_silencer { |line| line =~ /yolo/ } + end + + test "backtrace should not contain lines that match the silencers" do + assert_equal \ + [ "/other/class.rb" ], + @bc.clean([ "/mongrel/class.rb", "/other/class.rb", "/mongrel/stuff.rb", "/other/yolo.rb" ]) + end + + test "backtrace should only contain lines that match the silencers" do + assert_equal \ + [ "/mongrel/class.rb", "/mongrel/stuff.rb", "/other/yolo.rb" ], + @bc.clean([ "/mongrel/class.rb", "/other/class.rb", "/mongrel/stuff.rb", "/other/yolo.rb" ], + :noise) + end +end + class BacktraceCleanerFilterAndSilencerTest < ActiveSupport::TestCase def setup @bc = ActiveSupport::BacktraceCleaner.new diff --git a/activesupport/test/concern_test.rb b/activesupport/test/concern_test.rb index 912ce30c29..a74ee880b2 100644 --- a/activesupport/test/concern_test.rb +++ b/activesupport/test/concern_test.rb @@ -56,10 +56,6 @@ class ConcernTest < ActiveSupport::TestCase @klass.send(:include, Baz) assert_equal "baz", @klass.new.baz assert @klass.included_modules.include?(ConcernTest::Baz) - - @klass.send(:include, Baz) - assert_equal "baz", @klass.new.baz - assert @klass.included_modules.include?(ConcernTest::Baz) end def test_class_methods_are_extended @@ -68,12 +64,6 @@ class ConcernTest < ActiveSupport::TestCase assert_equal ConcernTest::Baz::ClassMethods, (class << @klass; self.included_modules; end)[0] end - def test_instance_methods_are_included - @klass.send(:include, Baz) - assert_equal "baz", @klass.new.baz - assert @klass.included_modules.include?(ConcernTest::Baz) - end - def test_included_block_is_ran @klass.send(:include, Baz) assert_equal true, @klass.included_ran @@ -91,4 +81,18 @@ class ConcernTest < ActiveSupport::TestCase @klass.send(:include, Foo) assert_equal [ConcernTest::Foo, ConcernTest::Bar, ConcernTest::Baz], @klass.included_modules[0..2] end + + def test_raise_on_multiple_included_calls + assert_raises(ActiveSupport::Concern::MultipleIncludedBlocks) do + Module.new do + extend ActiveSupport::Concern + + included do + end + + included do + end + end + end + end end diff --git a/activesupport/test/configurable_test.rb b/activesupport/test/configurable_test.rb index 215a6e06b0..d00273a028 100644 --- a/activesupport/test/configurable_test.rb +++ b/activesupport/test/configurable_test.rb @@ -38,7 +38,7 @@ class ConfigurableActiveSupport < ActiveSupport::TestCase assert_equal :bar, Parent.config.foo end - test "configuration accessors is not available on instance" do + test "configuration accessors are not available on instance" do instance = Parent.new assert !instance.respond_to?(:bar) diff --git a/activesupport/test/constantize_test_cases.rb b/activesupport/test/constantize_test_cases.rb index 9b62295c96..bbeb710a0c 100644 --- a/activesupport/test/constantize_test_cases.rb +++ b/activesupport/test/constantize_test_cases.rb @@ -34,8 +34,6 @@ module ConstantizeTestCases assert_equal Case::Dice, yield("Object::Case::Dice") assert_equal ConstantizeTestCases, yield("ConstantizeTestCases") assert_equal ConstantizeTestCases, yield("::ConstantizeTestCases") - assert_equal Object, yield("") - assert_equal Object, yield("::") assert_raises(NameError) { yield("UnknownClass") } assert_raises(NameError) { yield("UnknownClass::Ace") } assert_raises(NameError) { yield("UnknownClass::Ace::Base") } @@ -45,6 +43,8 @@ module ConstantizeTestCases assert_raises(NameError) { yield("Ace::Base::ConstantizeTestCases") } assert_raises(NameError) { yield("Ace::Gas::Base") } assert_raises(NameError) { yield("Ace::Gas::ConstantizeTestCases") } + assert_raises(NameError) { yield("") } + assert_raises(NameError) { yield("::") } end def run_safe_constantize_tests_on @@ -58,8 +58,8 @@ module ConstantizeTestCases assert_equal Case::Dice, yield("Object::Case::Dice") assert_equal ConstantizeTestCases, yield("ConstantizeTestCases") assert_equal ConstantizeTestCases, yield("::ConstantizeTestCases") - assert_equal Object, yield("") - assert_equal Object, yield("::") + assert_nil yield("") + assert_nil yield("::") assert_nil yield("UnknownClass") assert_nil yield("UnknownClass::Ace") assert_nil yield("UnknownClass::Ace::Base") diff --git a/activesupport/test/core_ext/array_ext_test.rb b/activesupport/test/core_ext/array_ext_test.rb index efa7582ab0..6cd0eb39b7 100644 --- a/activesupport/test/core_ext/array_ext_test.rb +++ b/activesupport/test/core_ext/array_ext_test.rb @@ -96,6 +96,10 @@ class ArrayExtToSentenceTests < ActiveSupport::TestCase assert_equal "one two, and three", ['one', 'two', 'three'].to_sentence(options) assert_equal({ words_connector: ' ' }, options) end + + def test_with_blank_elements + assert_equal ", one, , two, and three", [nil, 'one', '', 'two', 'three'].to_sentence + end end class ArrayExtToSTests < ActiveSupport::TestCase @@ -355,36 +359,6 @@ class ArrayExtractOptionsTests < ActiveSupport::TestCase end end -class ArrayUniqByTests < ActiveSupport::TestCase - def test_uniq_by - ActiveSupport::Deprecation.silence do - assert_equal [1,2], [1,2,3,4].uniq_by { |i| i.odd? } - assert_equal [1,2], [1,2,3,4].uniq_by(&:even?) - assert_equal((-5..0).to_a, (-5..5).to_a.uniq_by{ |i| i**2 }) - end - end - - def test_uniq_by! - a = [1,2,3,4] - ActiveSupport::Deprecation.silence do - a.uniq_by! { |i| i.odd? } - end - assert_equal [1,2], a - - a = [1,2,3,4] - ActiveSupport::Deprecation.silence do - a.uniq_by! { |i| i.even? } - end - assert_equal [1,2], a - - a = (-5..5).to_a - ActiveSupport::Deprecation.silence do - a.uniq_by! { |i| i**2 } - end - assert_equal((-5..0).to_a, a) - end -end - class ArrayWrapperTests < ActiveSupport::TestCase class FakeCollection def to_ary diff --git a/activesupport/test/core_ext/bigdecimal_test.rb b/activesupport/test/core_ext/bigdecimal_test.rb index a5987044b9..b386e55d6c 100644 --- a/activesupport/test/core_ext/bigdecimal_test.rb +++ b/activesupport/test/core_ext/bigdecimal_test.rb @@ -1,5 +1,4 @@ require 'abstract_unit' -require 'bigdecimal' require 'active_support/core_ext/big_decimal' class BigDecimalTest < ActiveSupport::TestCase diff --git a/activesupport/test/core_ext/class/attribute_accessor_test.rb b/activesupport/test/core_ext/class/attribute_accessor_test.rb index 8d827f054e..0d5f39a72b 100644 --- a/activesupport/test/core_ext/class/attribute_accessor_test.rb +++ b/activesupport/test/core_ext/class/attribute_accessor_test.rb @@ -44,16 +44,18 @@ class ClassAttributeAccessorTest < ActiveSupport::TestCase end def test_should_raise_name_error_if_attribute_name_is_invalid - assert_raises NameError do + exception = assert_raises NameError do Class.new do - cattr_reader "invalid attribute name" + cattr_reader "1nvalid" end end + assert_equal "invalid class attribute name: 1nvalid", exception.message - assert_raises NameError do + exception = assert_raises NameError do Class.new do - cattr_writer "invalid attribute name" + cattr_writer "1nvalid" end end + assert_equal "invalid class attribute name: 1nvalid", exception.message end end diff --git a/activesupport/test/core_ext/class/attribute_test.rb b/activesupport/test/core_ext/class/attribute_test.rb index 1c3ba8a7a0..e7a1334db3 100644 --- a/activesupport/test/core_ext/class/attribute_test.rb +++ b/activesupport/test/core_ext/class/attribute_test.rb @@ -27,7 +27,7 @@ class ClassAttributeTest < ActiveSupport::TestCase assert_equal 1, Class.new(@sub).setting end - test 'query method' do + test 'predicate method' do assert_equal false, @klass.setting? @klass.setting = 1 assert_equal true, @klass.setting? @@ -48,7 +48,7 @@ class ClassAttributeTest < ActiveSupport::TestCase assert_equal 1, object.setting end - test 'instance query' do + test 'instance predicate' do object = @klass.new assert_equal false, object.setting? object.setting = 1 @@ -73,6 +73,11 @@ class ClassAttributeTest < ActiveSupport::TestCase assert_raise(NoMethodError) { object.setting = 'boom' } end + test 'disabling instance predicate' do + object = Class.new { class_attribute :setting, instance_predicate: false }.new + assert_raise(NoMethodError) { object.setting? } + end + test 'works well with singleton classes' do object = @klass.new object.singleton_class.setting = 'foo' diff --git a/activesupport/test/core_ext/date_and_time_behavior.rb b/activesupport/test/core_ext/date_and_time_behavior.rb index 9927856aa2..b4ef5a0597 100644 --- a/activesupport/test/core_ext/date_and_time_behavior.rb +++ b/activesupport/test/core_ext/date_and_time_behavior.rb @@ -95,6 +95,11 @@ module DateAndTimeBehavior end def test_next_week + # M | T | W | T | F | S | S # M | T | W | T | F | S | S # + # | 22/2 | | | | | # 28/2 | | | | | | # monday in next week `next_week` + # | 22/2 | | | | | # | | | | 4/3 | | # friday in next week `next_week(:friday)` + # 23/10 | | | | | | # 30/10 | | | | | | # monday in next week `next_week` + # 23/10 | | | | | | # | | 1/11 | | | | # wednesday in next week `next_week(:wednesday)` assert_equal date_time_init(2005,2,28,0,0,0), date_time_init(2005,2,22,15,15,10).next_week assert_equal date_time_init(2005,3,4,0,0,0), date_time_init(2005,2,22,15,15,10).next_week(:friday) assert_equal date_time_init(2006,10,30,0,0,0), date_time_init(2006,10,23,0,0,0).next_week diff --git a/activesupport/test/core_ext/date_ext_test.rb b/activesupport/test/core_ext/date_ext_test.rb index 7ae1f67785..eab3aa7a6e 100644 --- a/activesupport/test/core_ext/date_ext_test.rb +++ b/activesupport/test/core_ext/date_ext_test.rb @@ -25,6 +25,7 @@ class DateExtCalculationsTest < ActiveSupport::TestCase assert_equal "February 21st, 2005", date.to_s(:long_ordinal) assert_equal "2005-02-21", date.to_s(:db) assert_equal "21 Feb 2005", date.to_s(:rfc822) + assert_equal "2005-02-21", date.to_s(:iso8601) end def test_readable_inspect @@ -33,8 +34,12 @@ class DateExtCalculationsTest < ActiveSupport::TestCase end def test_to_time - assert_equal Time.local(2005, 2, 21), Date.new(2005, 2, 21).to_time - assert_equal Time.local_time(2039, 2, 21), Date.new(2039, 2, 21).to_time + with_env_tz 'US/Eastern' do + assert_equal Time, Date.new(2005, 2, 21).to_time.class + assert_equal Time.local(2005, 2, 21), Date.new(2005, 2, 21).to_time + assert_equal Time.local(2005, 2, 21).utc_offset, Date.new(2005, 2, 21).to_time.utc_offset + end + silence_warnings do 0.upto(138) do |year| [:utc, :local].each do |format| @@ -44,6 +49,10 @@ class DateExtCalculationsTest < ActiveSupport::TestCase end end + def test_compare_to_time + assert Date.yesterday < Time.now + end + def test_to_datetime assert_equal DateTime.civil(2005, 2, 21), Date.new(2005, 2, 21).to_datetime assert_equal 0, Date.new(2005, 2, 21).to_datetime.offset # use UTC offset @@ -239,6 +248,10 @@ class DateExtCalculationsTest < ActiveSupport::TestCase assert_equal Time.local(2005,2,21,0,0,0), Date.new(2005,2,21).beginning_of_day end + def test_middle_of_day + assert_equal Time.local(2005,2,21,12,0,0), Date.new(2005,2,21).middle_of_day + end + def test_beginning_of_day_when_zone_is_set zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)'] with_env_tz 'UTC' do @@ -354,3 +367,4 @@ class DateExtBehaviorTest < ActiveSupport::TestCase end end end + diff --git a/activesupport/test/core_ext/date_time_ext_test.rb b/activesupport/test/core_ext/date_time_ext_test.rb index 3353465c1c..0a40aeb96c 100644 --- a/activesupport/test/core_ext/date_time_ext_test.rb +++ b/activesupport/test/core_ext/date_time_ext_test.rb @@ -18,6 +18,12 @@ class DateTimeExtCalculationsTest < ActiveSupport::TestCase assert_equal "Mon, 21 Feb 2005 14:30:00 +0000", datetime.to_s(:rfc822) assert_equal "February 21st, 2005 14:30", datetime.to_s(:long_ordinal) assert_match(/^2005-02-21T14:30:00(Z|\+00:00)$/, datetime.to_s) + + with_env_tz "US/Central" do + assert_equal "2009-02-05T14:30:05-06:00", DateTime.civil(2009, 2, 5, 14, 30, 5, Rational(-21600, 86400)).to_s(:iso8601) + assert_equal "2008-06-09T04:05:01-05:00", DateTime.civil(2008, 6, 9, 4, 5, 1, Rational(-18000, 86400)).to_s(:iso8601) + assert_equal "2009-02-05T14:30:05+00:00", DateTime.civil(2009, 2, 5, 14, 30, 5).to_s(:iso8601) + end end def test_readable_inspect @@ -41,11 +47,14 @@ class DateTimeExtCalculationsTest < ActiveSupport::TestCase end def test_to_time - assert_equal Time.utc(2005, 2, 21, 10, 11, 12), DateTime.new(2005, 2, 21, 10, 11, 12, 0).to_time - assert_equal Time.utc_time(2039, 2, 21, 10, 11, 12), DateTime.new(2039, 2, 21, 10, 11, 12, 0).to_time - # DateTimes with offsets other than 0 are returned unaltered - assert_equal DateTime.new(2005, 2, 21, 10, 11, 12, Rational(-5, 24)), DateTime.new(2005, 2, 21, 10, 11, 12, Rational(-5, 24)).to_time - # Fractional seconds are preserved + with_env_tz 'US/Eastern' do + assert_equal Time, DateTime.new(2005, 2, 21, 10, 11, 12, 0).to_time.class + assert_equal Time.local(2005, 2, 21, 5, 11, 12), DateTime.new(2005, 2, 21, 10, 11, 12, 0).to_time + assert_equal Time.local(2005, 2, 21, 5, 11, 12).utc_offset, DateTime.new(2005, 2, 21, 10, 11, 12, 0).to_time.utc_offset + end + end + + def test_to_time_preserves_fractional_seconds assert_equal Time.utc(2005, 2, 21, 10, 11, 12, 256), DateTime.new(2005, 2, 21, 10, 11, 12 + Rational(256, 1000000), 0).to_time end @@ -73,6 +82,10 @@ class DateTimeExtCalculationsTest < ActiveSupport::TestCase assert_equal DateTime.civil(2005,2,4,0,0,0), DateTime.civil(2005,2,4,10,10,10).beginning_of_day end + def test_middle_of_day + assert_equal DateTime.civil(2005,2,4,12,0,0), DateTime.civil(2005,2,4,10,10,10).middle_of_day + end + def test_end_of_day assert_equal DateTime.civil(2005,2,4,23,59,59), DateTime.civil(2005,2,4,10,10,10).end_of_day end @@ -85,6 +98,14 @@ class DateTimeExtCalculationsTest < ActiveSupport::TestCase assert_equal DateTime.civil(2005,2,4,19,59,59), DateTime.civil(2005,2,4,19,30,10).end_of_hour end + def test_beginning_of_minute + assert_equal DateTime.civil(2005,2,4,19,30,0), DateTime.civil(2005,2,4,19,30,10).beginning_of_minute + end + + def test_end_of_minute + assert_equal DateTime.civil(2005,2,4,19,30,59), DateTime.civil(2005,2,4,19,30,10).end_of_minute + end + def test_end_of_month assert_equal DateTime.civil(2005,3,31,23,59,59), DateTime.civil(2005,3,20,10,10,10).end_of_month assert_equal DateTime.civil(2005,2,28,23,59,59), DateTime.civil(2005,2,20,10,10,10).end_of_month @@ -118,6 +139,9 @@ class DateTimeExtCalculationsTest < ActiveSupport::TestCase assert_equal DateTime.civil(2005,2,22,16), DateTime.civil(2005,2,22,15,15,10).change(:hour => 16) assert_equal DateTime.civil(2005,2,22,16,45), DateTime.civil(2005,2,22,15,15,10).change(:hour => 16, :min => 45) assert_equal DateTime.civil(2005,2,22,15,45), DateTime.civil(2005,2,22,15,15,10).change(:min => 45) + + # datetime with fractions of a second + assert_equal DateTime.civil(2005,2,1,15,15,10.7), DateTime.civil(2005,2,22,15,15,10.7).change(:day => 1) end def test_advance @@ -242,6 +266,10 @@ class DateTimeExtCalculationsTest < ActiveSupport::TestCase end end + def test_acts_like_date + assert DateTime.new.acts_like_date? + end + def test_acts_like_time assert DateTime.new.acts_like_time? end @@ -309,6 +337,16 @@ class DateTimeExtCalculationsTest < ActiveSupport::TestCase assert_equal 946684800, DateTime.civil(1999,12,31,19,0,0,Rational(-5,24)).to_i end + def test_usec + assert_equal 0, DateTime.civil(2000).usec + assert_equal 500000, DateTime.civil(2000, 1, 1, 0, 0, Rational(1,2)).usec + end + + def test_nsec + assert_equal 0, DateTime.civil(2000).nsec + assert_equal 500000000, DateTime.civil(2000, 1, 1, 0, 0, Rational(1,2)).nsec + end + protected def with_env_tz(new_tz = 'US/Eastern') old_tz, ENV['TZ'] = ENV['TZ'], new_tz diff --git a/activesupport/test/core_ext/duration_test.rb b/activesupport/test/core_ext/duration_test.rb index 2826f51f2d..ed267cf4b9 100644 --- a/activesupport/test/core_ext/duration_test.rb +++ b/activesupport/test/core_ext/duration_test.rb @@ -27,6 +27,7 @@ class DurationTest < ActiveSupport::TestCase 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') end @@ -37,6 +38,8 @@ class DurationTest < ActiveSupport::TestCase assert_equal '6 months and -2 days', (6.months - 2.days).inspect assert_equal '10 seconds', 10.seconds.inspect assert_equal '10 years, 2 months, and 1 day', (10.years + 2.months + 1.day).inspect + assert_equal '10 years, 2 months, and 1 day', (10.years + 1.month + 1.day + 1.month).inspect + assert_equal '10 years, 2 months, and 1 day', (1.day + 10.years + 2.months).inspect assert_equal '7 days', 1.week.inspect assert_equal '14 days', 1.fortnight.inspect end @@ -50,14 +53,12 @@ class DurationTest < ActiveSupport::TestCase end def test_argument_error - begin - 1.second.ago('') - flunk("no exception was raised") - rescue ArgumentError => e - assert_equal 'expected a time or date, got ""', e.message, "ensure ArgumentError is not being raised by dependencies.rb" - rescue Exception => e - flunk("ArgumentError should be raised, but we got #{e.class} instead") - end + 1.second.ago('') + flunk("no exception was raised") + rescue ArgumentError => e + assert_equal 'expected a time or date, got ""', e.message, "ensure ArgumentError is not being raised by dependencies.rb" + rescue Exception => e + flunk("ArgumentError should be raised, but we got #{e.class} instead") end def test_fractional_weeks diff --git a/activesupport/test/core_ext/enumerable_test.rb b/activesupport/test/core_ext/enumerable_test.rb index 0a1abac767..6781e3c20e 100644 --- a/activesupport/test/core_ext/enumerable_test.rb +++ b/activesupport/test/core_ext/enumerable_test.rb @@ -24,10 +24,8 @@ class EnumerableTests < ActiveSupport::TestCase def test_group_by names = %w(marcel sam david jeremy) klass = Struct.new(:name) - objects = (1..50).inject([]) do |people,| - p = klass.new - p.name = names.sort_by { rand }.first - people << p + objects = (1..50).map do + klass.new names.sample end enum = GenericEnumerable.new(objects) diff --git a/activesupport/test/core_ext/hash_ext_test.rb b/activesupport/test/core_ext/hash_ext_test.rb index c378dcd01d..b059bc3e89 100644 --- a/activesupport/test/core_ext/hash_ext_test.rb +++ b/activesupport/test/core_ext/hash_ext_test.rb @@ -8,7 +8,7 @@ require 'active_support/core_ext/object/deep_dup' require 'active_support/inflections' class HashExtTest < ActiveSupport::TestCase - class IndifferentHash < HashWithIndifferentAccess + class IndifferentHash < ActiveSupport::HashWithIndifferentAccess end class SubclassingArray < Array @@ -480,6 +480,42 @@ class HashExtTest < ActiveSupport::TestCase assert_equal hash.delete('a'), nil end + def test_indifferent_select + hash = ActiveSupport::HashWithIndifferentAccess.new(@strings).select {|k,v| v == 1} + + assert_equal({ 'a' => 1 }, hash) + assert_instance_of ActiveSupport::HashWithIndifferentAccess, hash + end + + def test_indifferent_select_returns_a_hash_when_unchanged + hash = ActiveSupport::HashWithIndifferentAccess.new(@strings).select {|k,v| true} + + assert_instance_of ActiveSupport::HashWithIndifferentAccess, hash + end + + def test_indifferent_select_bang + indifferent_strings = ActiveSupport::HashWithIndifferentAccess.new(@strings) + indifferent_strings.select! {|k,v| v == 1} + + assert_equal({ 'a' => 1 }, indifferent_strings) + assert_instance_of ActiveSupport::HashWithIndifferentAccess, indifferent_strings + end + + def test_indifferent_reject + hash = ActiveSupport::HashWithIndifferentAccess.new(@strings).reject {|k,v| v != 1} + + assert_equal({ 'a' => 1 }, hash) + assert_instance_of ActiveSupport::HashWithIndifferentAccess, hash + end + + def test_indifferent_reject_bang + indifferent_strings = ActiveSupport::HashWithIndifferentAccess.new(@strings) + indifferent_strings.reject! {|k,v| v != 1} + + assert_equal({ 'a' => 1 }, indifferent_strings) + assert_instance_of ActiveSupport::HashWithIndifferentAccess, indifferent_strings + end + def test_indifferent_to_hash # Should convert to a Hash with String keys. assert_equal @strings, @mixed.with_indifferent_access.to_hash @@ -490,6 +526,10 @@ class HashExtTest < ActiveSupport::TestCase roundtrip = mixed_with_default.with_indifferent_access.to_hash assert_equal @strings, roundtrip assert_equal '1234', roundtrip.default + 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) + assert_not new_to_hash["a"]["b"].instance_of?(HashWithIndifferentAccess) end def test_lookup_returns_the_same_object_that_is_stored_in_hash_indifferent_access @@ -499,9 +539,21 @@ class HashExtTest < ActiveSupport::TestCase assert_equal [1], hash[:a] end + def test_with_indifferent_access_has_no_side_effects_on_existing_hash + hash = {content: [{:foo => :bar, 'bar' => 'baz'}]} + hash.with_indifferent_access + + assert_equal [:foo, "bar"], hash[:content].first.keys + end + def test_indifferent_hash_with_array_of_hashes hash = { "urls" => { "url" => [ { "address" => "1" }, { "address" => "2" } ] }}.with_indifferent_access assert_equal "1", hash[:urls][:url].first[:address] + + hash = hash.to_hash + assert_not hash.instance_of?(HashWithIndifferentAccess) + assert_not hash["urls"].instance_of?(HashWithIndifferentAccess) + assert_not hash["urls"]["url"].first.instance_of?(HashWithIndifferentAccess) end def test_should_preserve_array_subclass_when_value_is_array @@ -655,12 +707,6 @@ class HashExtTest < ActiveSupport::TestCase assert_equal expected, merged end - def test_diff - assert_deprecated do - assert_equal({ :a => 2 }, { :a => 2, :b => 5 }.diff({ :a => 1, :b => 5 })) - end - end - def test_slice original = { :a => 'x', :b => 'y', :c => 10 } expected = { :a => 'x', :b => 'y' } @@ -735,6 +781,24 @@ class HashExtTest < ActiveSupport::TestCase assert_equal 'bender', slice['login'] end + def test_slice_bang_does_not_override_default + hash = Hash.new(0) + hash.update(a: 1, b: 2) + + hash.slice!(:a) + + assert_equal 0, hash[:c] + end + + def test_slice_bang_does_not_override_default_proc + hash = Hash.new { |h, k| h[k] = [] } + hash.update(a: 1, b: 2) + + hash.slice!(:a) + + assert_equal [], hash[:c] + end + def test_extract original = {:a => 1, :b => 2, :c => 3, :d => 4} expected = {:a => 1, :b => 2} @@ -1015,12 +1079,10 @@ class HashToXmlTest < ActiveSupport::TestCase <replies-close-in type="integer">2592000000</replies-close-in> <written-on type="date">2003-07-16</written-on> <viewed-at type="datetime">2003-07-16T09:28:00+0000</viewed-at> - <content type="yaml">--- \n1: should be an integer\n:message: Have a nice day\narray: \n- should-have-dashes: true\n should_have_underscores: true\n</content> <author-email-address>david@loudthinking.com</author-email-address> <parent-id></parent-id> <ad-revenue type="decimal">1.5</ad-revenue> <optimum-viewing-angle type="float">135</optimum-viewing-angle> - <resident type="symbol">yes</resident> </topic> EOT @@ -1033,12 +1095,10 @@ class HashToXmlTest < ActiveSupport::TestCase :replies_close_in => 2592000000, :written_on => Date.new(2003, 7, 16), :viewed_at => Time.utc(2003, 7, 16, 9, 28), - :content => { :message => "Have a nice day", 1 => "should be an integer", "array" => [{ "should-have-dashes" => true, "should_have_underscores" => true }] }, :author_email_address => "david@loudthinking.com", :parent_id => nil, :ad_revenue => BigDecimal("1.50"), :optimum_viewing_angle => 135.0, - :resident => :yes }.stringify_keys assert_equal expected_topic_hash, Hash.from_xml(topic_xml)["topic"] @@ -1052,7 +1112,6 @@ class HashToXmlTest < ActiveSupport::TestCase <approved type="boolean"></approved> <written-on type="date"></written-on> <viewed-at type="datetime"></viewed-at> - <content type="yaml"></content> <parent-id></parent-id> </topic> EOT @@ -1063,7 +1122,6 @@ class HashToXmlTest < ActiveSupport::TestCase :approved => nil, :written_on => nil, :viewed_at => nil, - :content => nil, :parent_id => nil }.stringify_keys @@ -1290,6 +1348,28 @@ class HashToXmlTest < ActiveSupport::TestCase assert_equal expected_product_hash, Hash.from_xml(product_xml)["product"] end + def test_from_xml_raises_on_disallowed_type_attributes + assert_raise ActiveSupport::XMLConverter::DisallowedType do + Hash.from_xml '<product><name type="foo">value</name></product>', %w(foo) + end + end + + def test_from_xml_disallows_symbol_and_yaml_types_by_default + assert_raise ActiveSupport::XMLConverter::DisallowedType do + Hash.from_xml '<product><name type="symbol">value</name></product>' + end + + assert_raise ActiveSupport::XMLConverter::DisallowedType do + Hash.from_xml '<product><name type="yaml">value</name></product>' + end + end + + def test_from_trusted_xml_allows_symbol_and_yaml_types + expected = { 'product' => { 'name' => :value }} + assert_equal expected, Hash.from_trusted_xml('<product><name type="symbol">value</name></product>') + assert_equal expected, Hash.from_trusted_xml('<product><name type="yaml">:value</name></product>') + end + def test_should_use_default_value_for_unknown_key hash_wia = HashWithIndifferentAccess.new(3) assert_equal 3, hash_wia[:new_key] @@ -1330,7 +1410,7 @@ class HashToXmlTest < ActiveSupport::TestCase def test_empty_string_works_for_typecast_xml_value assert_nothing_raised do - Hash.__send__(:typecast_xml_value, "") + ActiveSupport::XMLConverter.new("").to_h end end diff --git a/activesupport/test/core_ext/kernel_test.rb b/activesupport/test/core_ext/kernel_test.rb index 1583c1fa32..b8951de402 100644 --- a/activesupport/test/core_ext/kernel_test.rb +++ b/activesupport/test/core_ext/kernel_test.rb @@ -38,6 +38,18 @@ class KernelTest < ActiveSupport::TestCase # Skip if we can't STDERR.tell end + def test_quietly + old_stdout_position, old_stderr_position = STDOUT.tell, STDERR.tell + quietly do + puts 'see me, feel me' + STDERR.puts 'touch me, heal me' + end + assert_equal old_stdout_position, STDOUT.tell + assert_equal old_stderr_position, STDERR.tell + rescue Errno::ESPIPE + # Skip if we can't STDERR.tell + end + def test_silence_stderr_with_return_value assert_equal 1, silence_stderr { 1 } end diff --git a/activesupport/test/core_ext/marshal_test.rb b/activesupport/test/core_ext/marshal_test.rb index ac79b15fa8..8f3f710dfd 100644 --- a/activesupport/test/core_ext/marshal_test.rb +++ b/activesupport/test/core_ext/marshal_test.rb @@ -1,10 +1,10 @@ require 'abstract_unit' require 'active_support/core_ext/marshal' -require 'dependecies_test_helpers' +require 'dependencies_test_helpers' class MarshalTest < ActiveSupport::TestCase include ActiveSupport::Testing::Isolation - include DependeciesTestHelpers + include DependenciesTestHelpers def teardown ActiveSupport::Dependencies.clear diff --git a/activesupport/test/core_ext/module_test.rb b/activesupport/test/core_ext/module_test.rb index 82249ddd1b..283b13ff8b 100644 --- a/activesupport/test/core_ext/module_test.rb +++ b/activesupport/test/core_ext/module_test.rb @@ -66,6 +66,23 @@ Tester = Struct.new(:client) do delegate :name, :to => :client, :prefix => false end +Product = Struct.new(:name) do + delegate :name, :to => :manufacturer, :prefix => true + delegate :name, :to => :type, :prefix => true + + def manufacturer + @manufacturer ||= begin + nil.unknown_method + end + end + + def type + @type ||= begin + nil.type_name + end + end +end + class ParameterSet delegate :[], :[]=, :to => :@params @@ -82,6 +99,21 @@ class Name end end +class SideEffect + attr_reader :ints + + delegate :to_i, :to => :shift, :allow_nil => true + delegate :to_s, :to => :shift + + def initialize + @ints = [1, 2, 3] + end + + def shift + @ints.shift + end +end + class ModuleTest < ActiveSupport::TestCase def setup @david = Someone.new("David", Somewhere.new("Paulina", "Chicago")) @@ -171,6 +203,17 @@ class ModuleTest < ActiveSupport::TestCase assert_nil rails.name end + # Ensures with check for nil, not for a falseish target. + def test_delegation_with_allow_nil_and_false_value + project = Project.new(false, false) + assert_raise(NoMethodError) { project.name } + end + + def test_delegation_with_allow_nil_and_invalid_value + rails = Project.new("Rails", "David") + assert_raise(NoMethodError) { rails.name } + end + def test_delegation_with_allow_nil_and_nil_value_and_prefix Project.class_eval do delegate :name, :to => :person, :allow_nil => true, :prefix => true @@ -181,7 +224,7 @@ class ModuleTest < ActiveSupport::TestCase def test_delegation_without_allow_nil_and_nil_value david = Someone.new("David") - assert_raise(RuntimeError) { david.street } + assert_raise(Module::DelegationError) { david.street } end def test_delegation_to_method_that_exists_on_nil @@ -228,6 +271,26 @@ class ModuleTest < ActiveSupport::TestCase "[#{e.backtrace.inspect}] did not include [#{file_and_line}]" end + def test_delegation_invokes_the_target_exactly_once + se = SideEffect.new + + assert_equal 1, se.to_i + assert_equal [2, 3], se.ints + + assert_equal '2', se.to_s + assert_equal [3], se.ints + end + + def test_delegation_doesnt_mask_nested_no_method_error_on_nil_receiver + product = Product.new('Widget') + + # Nested NoMethodError is a different name from the delegation + assert_raise(NoMethodError) { product.manufacturer_name } + + # Nested NoMethodError is the same name as the delegation + assert_raise(NoMethodError) { product.type_name } + end + def test_parent assert_equal Yz::Zy, Yz::Zy::Cd.parent assert_equal Yz, Yz::Zy.parent @@ -242,12 +305,6 @@ class ModuleTest < ActiveSupport::TestCase def test_local_constants assert_equal %w(Constant1 Constant3), Ab.local_constants.sort.map(&:to_s) end - - def test_local_constant_names - ActiveSupport::Deprecation.silence do - assert_equal %w(Constant1 Constant3), Ab.local_constant_names.sort.map(&:to_s) - end - end end module BarMethodAliaser diff --git a/activesupport/test/core_ext/numeric_ext_test.rb b/activesupport/test/core_ext/numeric_ext_test.rb index 435f4aa5a1..1da72eb17d 100644 --- a/activesupport/test/core_ext/numeric_ext_test.rb +++ b/activesupport/test/core_ext/numeric_ext_test.rb @@ -77,7 +77,7 @@ class NumericExtTimeAndDateTimeTest < ActiveSupport::TestCase assert_equal @dtnow.advance(:days => 1).advance(:months => 2), @dtnow + 1.day + 2.months end - def test_duration_after_convertion_is_no_longer_accurate + def test_duration_after_conversion_is_no_longer_accurate assert_equal 30.days.to_i.since(@now), 1.month.to_i.since(@now) assert_equal 365.25.days.to_f.since(@now), 1.year.to_f.since(@now) assert_equal 30.days.to_i.since(@dtnow), 1.month.to_i.since(@dtnow) @@ -153,22 +153,16 @@ end class NumericExtSizeTest < ActiveSupport::TestCase def test_unit_in_terms_of_another - relationships = { - 1024.bytes => 1.kilobyte, - 1024.kilobytes => 1.megabyte, - 3584.0.kilobytes => 3.5.megabytes, - 3584.0.megabytes => 3.5.gigabytes, - 1.kilobyte ** 4 => 1.terabyte, - 1024.kilobytes + 2.megabytes => 3.megabytes, - 2.gigabytes / 4 => 512.megabytes, - 256.megabytes * 20 + 5.gigabytes => 10.gigabytes, - 1.kilobyte ** 5 => 1.petabyte, - 1.kilobyte ** 6 => 1.exabyte - } - - relationships.each do |left, right| - assert_equal right, left - end + assert_equal 1024.bytes, 1.kilobyte + assert_equal 1024.kilobytes, 1.megabyte + assert_equal 3584.0.kilobytes, 3.5.megabytes + assert_equal 3584.0.megabytes, 3.5.gigabytes + assert_equal 1.kilobyte ** 4, 1.terabyte + assert_equal 1024.kilobytes + 2.megabytes, 3.megabytes + assert_equal 2.gigabytes / 4, 512.megabytes + assert_equal 256.megabytes * 20 + 5.gigabytes, 10.gigabytes + assert_equal 1.kilobyte ** 5, 1.petabyte + assert_equal 1.kilobyte ** 6, 1.exabyte end def test_units_as_bytes_independently @@ -203,7 +197,7 @@ class NumericExtFormattingTest < ActiveSupport::TestCase def terabytes(number) gigabytes(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)) @@ -217,7 +211,7 @@ class NumericExtFormattingTest < ActiveSupport::TestCase assert_equal("22-555-1212", 225551212.to_s(:phone)) assert_equal("+45-22-555-1212", 225551212.to_s(:phone, :country_code => 45)) end - + def test_to_s__currency assert_equal("$1,234,567,890.50", 1234567890.50.to_s(:currency)) assert_equal("$1,234,567,890.51", 1234567890.506.to_s(:currency)) @@ -228,8 +222,8 @@ class NumericExtFormattingTest < ActiveSupport::TestCase assert_equal("$1,234,567,890.5", 1234567890.50.to_s(:currency, :precision => 1)) assert_equal("£1234567890,50", 1234567890.50.to_s(:currency, :unit => "£", :separator => ",", :delimiter => "")) end - - + + def test_to_s__rounded assert_equal("-111.235", -111.2346.to_s(:rounded)) assert_equal("111.235", 111.2346.to_s(:rounded)) @@ -246,7 +240,7 @@ class NumericExtFormattingTest < ActiveSupport::TestCase assert_equal("11.00", 10.995.to_s(:rounded, :precision => 2)) assert_equal("0.00", -0.001.to_s(:rounded, :precision => 2)) end - + def test_to_s__percentage assert_equal("100.000%", 100.to_s(:percentage)) assert_equal("100%", 100.to_s(:percentage, :precision => 0)) @@ -274,7 +268,7 @@ class NumericExtFormattingTest < ActiveSupport::TestCase assert_equal '12.345.678,05', 12345678.05.to_s(:delimited, :separator => ',', :delimiter => '.') assert_equal '12.345.678,05', 12345678.05.to_s(:delimited, :delimiter => '.', :separator => ',') end - + def test_to_s__rounded_with_custom_delimiter_and_separator assert_equal '31,83', 31.825.to_s(:rounded, :precision => 2, :separator => ',') @@ -350,7 +344,7 @@ class NumericExtFormattingTest < ActiveSupport::TestCase assert_equal '1.23 GB', 1234567890.to_s(:human_size, :prefix => :si) assert_equal '1.23 TB', 1234567890123.to_s(:human_size, :prefix => :si) end - + def test_to_s__human_size_with_options_hash assert_equal '1.2 MB', 1234567.to_s(:human_size, :precision => 2) assert_equal '3 Bytes', 3.14159265.to_s(:human_size, :precision => 4) @@ -366,13 +360,13 @@ class NumericExtFormattingTest < ActiveSupport::TestCase assert_equal '1.012 KB', kilobytes(1.0123).to_s(:human_size, :precision => 3, :significant => false) assert_equal '1 KB', kilobytes(1.0123).to_s(:human_size, :precision => 0, :significant => true) #ignores significant it precision is 0 end - + def test_to_s__human_size_with_custom_delimiter_and_separator assert_equal '1,01 KB', kilobytes(1.0123).to_s(:human_size, :precision => 3, :separator => ',') assert_equal '1,01 KB', kilobytes(1.0100).to_s(:human_size, :precision => 4, :separator => ',') assert_equal '1.000,1 TB', terabytes(1000.1).to_s(:human_size, :precision => 5, :delimiter => '.', :separator => ',') end - + def test_number_to_human assert_equal '-123', -123.to_s(:human) assert_equal '-0.5', -0.5.to_s(:human) @@ -436,7 +430,7 @@ class NumericExtFormattingTest < ActiveSupport::TestCase def test_to_s__injected_on_proper_types assert_equal Fixnum, 1230.class assert_equal '1.23 Thousand', 1230.to_s(:human) - + assert_equal Float, Float(1230).class assert_equal '1.23 Thousand', Float(1230).to_s(:human) diff --git a/activesupport/test/core_ext/object/inclusion_test.rb b/activesupport/test/core_ext/object/inclusion_test.rb index 22888333f5..478706eeae 100644 --- a/activesupport/test/core_ext/object/inclusion_test.rb +++ b/activesupport/test/core_ext/object/inclusion_test.rb @@ -2,16 +2,6 @@ require 'abstract_unit' require 'active_support/core_ext/object/inclusion' class InTest < ActiveSupport::TestCase - def test_in_multiple_args - assert :b.in?(:a,:b) - assert !:c.in?(:a,:b) - end - - def test_in_multiple_arrays - assert [1,2].in?([1,2],[2,3]) - assert ![1,2].in?([1,3],[2,1]) - end - def test_in_array assert 1.in?([1,2]) assert !3.in?([1,2]) diff --git a/activesupport/test/core_ext/object/to_query_test.rb b/activesupport/test/core_ext/object/to_query_test.rb index 8d1a8c628c..92f996f9a4 100644 --- a/activesupport/test/core_ext/object/to_query_test.rb +++ b/activesupport/test/core_ext/object/to_query_test.rb @@ -47,7 +47,7 @@ class ToQueryTest < ActiveSupport::TestCase end private - def assert_query_equal(expected, actual, message = nil) + def assert_query_equal(expected, actual) assert_equal expected.split('&'), actual.to_query.split('&') end end diff --git a/activesupport/test/core_ext/object_and_class_ext_test.rb b/activesupport/test/core_ext/object_and_class_ext_test.rb index ec7dd6d4fb..8d748791e3 100644 --- a/activesupport/test/core_ext/object_and_class_ext_test.rb +++ b/activesupport/test/core_ext/object_and_class_ext_test.rb @@ -120,6 +120,10 @@ class ObjectTryTest < ActiveSupport::TestCase assert_raise(NoMethodError) { @string.try!(method, 'llo', 'y') } end + def test_try_only_block_bang + assert_equal @string.reverse, @string.try! { |s| s.reverse } + end + def test_valid_method assert_equal 5, @string.try(:size) end diff --git a/activesupport/test/core_ext/proc_test.rb b/activesupport/test/core_ext/proc_test.rb deleted file mode 100644 index c4d5592196..0000000000 --- a/activesupport/test/core_ext/proc_test.rb +++ /dev/null @@ -1,14 +0,0 @@ -require 'abstract_unit' -require 'active_support/core_ext/proc' - -class ProcTests < ActiveSupport::TestCase - def test_bind_returns_method_with_changed_self - assert_deprecated do - block = Proc.new { self } - assert_equal self, block.call - bound_block = block.bind("hello") - assert_not_equal block, bound_block - assert_equal "hello", bound_block.call - end - end -end diff --git a/activesupport/test/core_ext/range_ext_test.rb b/activesupport/test/core_ext/range_ext_test.rb index f0cdc0bfd4..854b0a38bd 100644 --- a/activesupport/test/core_ext/range_ext_test.rb +++ b/activesupport/test/core_ext/range_ext_test.rb @@ -12,6 +12,11 @@ class RangeTest < ActiveSupport::TestCase date_range = Time.utc(2005, 12, 10, 15, 30)..Time.utc(2005, 12, 10, 17, 30) assert_equal "BETWEEN '2005-12-10 15:30:00' AND '2005-12-10 17:30:00'", date_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 + end def test_overlaps_last_inclusive assert((1..5).overlaps?(5..10)) @@ -49,7 +54,7 @@ class RangeTest < ActiveSupport::TestCase assert((1...10) === (1...10)) end - def test_should_compare_other_with_exlusive_end + def test_should_compare_other_with_exclusive_end assert((1..10) === (1...10)) end @@ -85,4 +90,26 @@ class RangeTest < ActiveSupport::TestCase 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) 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 {} + 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) {} + end + end + + def test_include_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).include?(twz) + end + end + end diff --git a/activesupport/test/core_ext/string_ext_test.rb b/activesupport/test/core_ext/string_ext_test.rb index 6720ed42f0..3cb66d4eec 100644 --- a/activesupport/test/core_ext/string_ext_test.rb +++ b/activesupport/test/core_ext/string_ext_test.rb @@ -11,13 +11,6 @@ require 'active_support/core_ext/string/strip' require 'active_support/core_ext/string/output_safety' require 'active_support/core_ext/string/indent' -module Ace - module Base - class Case - end - end -end - class StringInflectionsTest < ActiveSupport::TestCase include InflectorTestCases include ConstantizeTestCases @@ -87,6 +80,12 @@ class StringInflectionsTest < ActiveSupport::TestCase assert_equal('capital', 'Capital'.camelize(:lower)) end + def test_dasherize + UnderscoresToDashes.each do |underscored, dasherized| + assert_equal(dasherized, underscored.dasherize) + end + end + def test_underscore CamelToUnderscore.each do |camel, underscore| assert_equal(underscore, camel.underscore) @@ -161,30 +160,6 @@ class StringInflectionsTest < ActiveSupport::TestCase assert_equal 97, 'abc'.ord end - def test_string_to_time - assert_equal Time.utc(2005, 2, 27, 23, 50), "2005-02-27 23:50".to_time - assert_equal Time.local(2005, 2, 27, 23, 50), "2005-02-27 23:50".to_time(:local) - assert_equal Time.utc(2005, 2, 27, 23, 50, 19, 275038), "2005-02-27T23:50:19.275038".to_time - assert_equal Time.local(2005, 2, 27, 23, 50, 19, 275038), "2005-02-27T23:50:19.275038".to_time(:local) - assert_equal DateTime.civil(2039, 2, 27, 23, 50), "2039-02-27 23:50".to_time - assert_equal Time.local_time(2039, 2, 27, 23, 50), "2039-02-27 23:50".to_time(:local) - assert_equal Time.utc(2011, 2, 27, 23, 50), "2011-02-27 22:50 -0100".to_time - assert_nil "".to_time - end - - def test_string_to_datetime - assert_equal DateTime.civil(2039, 2, 27, 23, 50), "2039-02-27 23:50".to_datetime - assert_equal 0, "2039-02-27 23:50".to_datetime.offset # use UTC offset - assert_equal ::Date::ITALY, "2039-02-27 23:50".to_datetime.start # use Ruby's default start value - assert_equal DateTime.civil(2039, 2, 27, 23, 50, 19 + Rational(275038, 1000000), "-04:00"), "2039-02-27T23:50:19.275038-04:00".to_datetime - assert_nil "".to_datetime - end - - def test_string_to_date - assert_equal Date.new(2005, 2, 27), "2005-02-27".to_date - assert_nil "".to_date - end - def test_access s = "hello" assert_equal "h", s.at(0) @@ -247,10 +222,11 @@ class StringInflectionsTest < ActiveSupport::TestCase end def test_string_squish - original = %{ A string with tabs(\t\t), newlines(\n\n), and - many spaces( ). } + original = %{\u180E\u180E A string surrounded by unicode mongolian vowel separators, + with tabs(\t\t), newlines(\n\n), unicode nextlines(\u0085\u0085) and many spaces( ). \u180E\u180E} - expected = "A string with tabs( ), newlines( ), and many spaces( )." + expected = "A string surrounded by unicode mongolian vowel separators, " + + "with tabs( ), newlines( ), unicode nextlines( ) and many spaces( )." # Make sure squish returns what we expect: assert_equal original.squish, expected @@ -287,13 +263,18 @@ class StringInflectionsTest < ActiveSupport::TestCase end def test_truncate_multibyte - assert_equal "\354\225\204\353\246\254\353\236\221 \354\225\204\353\246\254 ...".force_encoding('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('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? end + + def test_remove + assert_equal "Summer", "Fast Summer".remove(/Fast /) + assert_equal "Summer", "Fast Summer".remove!(/Fast /) + end def test_constantize run_constantize_tests_on do |string| @@ -308,6 +289,186 @@ class StringInflectionsTest < ActiveSupport::TestCase end end +class StringConversionsTest < ActiveSupport::TestCase + def test_string_to_time + with_env_tz "Europe/Moscow" do + assert_equal Time.utc(2005, 2, 27, 23, 50), "2005-02-27 23:50".to_time(:utc) + assert_equal Time.local(2005, 2, 27, 23, 50), "2005-02-27 23:50".to_time + assert_equal Time.utc(2005, 2, 27, 23, 50, 19, 275038), "2005-02-27T23:50:19.275038".to_time(:utc) + assert_equal Time.local(2005, 2, 27, 23, 50, 19, 275038), "2005-02-27T23:50:19.275038".to_time + assert_equal Time.utc(2039, 2, 27, 23, 50), "2039-02-27 23:50".to_time(:utc) + assert_equal Time.local(2039, 2, 27, 23, 50), "2039-02-27 23:50".to_time + assert_equal Time.local(2011, 2, 27, 17, 50), "2011-02-27 13:50 -0100".to_time + assert_equal Time.utc(2011, 2, 27, 23, 50), "2011-02-27 22:50 -0100".to_time(:utc) + assert_equal Time.local(2005, 2, 27, 22, 50), "2005-02-27 14:50 -0500".to_time + assert_nil "".to_time + end + end + + def test_string_to_time_utc_offset + with_env_tz "US/Eastern" do + assert_equal 0, "2005-02-27 23:50".to_time(:utc).utc_offset + assert_equal(-18000, "2005-02-27 23:50".to_time.utc_offset) + assert_equal 0, "2005-02-27 22:50 -0100".to_time(:utc).utc_offset + assert_equal(-18000, "2005-02-27 22:50 -0100".to_time.utc_offset) + end + end + + def test_partial_string_to_time + with_env_tz "Europe/Moscow" do + now = Time.now + assert_equal Time.local(now.year, now.month, now.day, 23, 50), "23:50".to_time + assert_equal Time.utc(now.year, now.month, now.day, 23, 50), "23:50".to_time(:utc) + assert_equal Time.local(now.year, now.month, now.day, 18, 50), "13:50 -0100".to_time + assert_equal Time.utc(now.year, now.month, now.day, 23, 50), "22:50 -0100".to_time(:utc) + end + end + + def test_standard_time_string_to_time_when_current_time_is_standard_time + with_env_tz "US/Eastern" do + Time.stubs(:now).returns(Time.local(2012, 1, 1)) + assert_equal Time.local(2012, 1, 1, 10, 0), "2012-01-01 10:00".to_time + assert_equal Time.utc(2012, 1, 1, 10, 0), "2012-01-01 10:00".to_time(:utc) + assert_equal Time.local(2012, 1, 1, 13, 0), "2012-01-01 10:00 -0800".to_time + assert_equal Time.utc(2012, 1, 1, 18, 0), "2012-01-01 10:00 -0800".to_time(:utc) + assert_equal Time.local(2012, 1, 1, 10, 0), "2012-01-01 10:00 -0500".to_time + assert_equal Time.utc(2012, 1, 1, 15, 0), "2012-01-01 10:00 -0500".to_time(:utc) + assert_equal Time.local(2012, 1, 1, 5, 0), "2012-01-01 10:00 UTC".to_time + assert_equal Time.utc(2012, 1, 1, 10, 0), "2012-01-01 10:00 UTC".to_time(:utc) + assert_equal Time.local(2012, 1, 1, 13, 0), "2012-01-01 10:00 PST".to_time + assert_equal Time.utc(2012, 1, 1, 18, 0), "2012-01-01 10:00 PST".to_time(:utc) + assert_equal Time.local(2012, 1, 1, 10, 0), "2012-01-01 10:00 EST".to_time + assert_equal Time.utc(2012, 1, 1, 15, 0), "2012-01-01 10:00 EST".to_time(:utc) + end + end + + def test_standard_time_string_to_time_when_current_time_is_daylight_savings + with_env_tz "US/Eastern" do + Time.stubs(:now).returns(Time.local(2012, 7, 1)) + assert_equal Time.local(2012, 1, 1, 10, 0), "2012-01-01 10:00".to_time + assert_equal Time.utc(2012, 1, 1, 10, 0), "2012-01-01 10:00".to_time(:utc) + assert_equal Time.local(2012, 1, 1, 13, 0), "2012-01-01 10:00 -0800".to_time + assert_equal Time.utc(2012, 1, 1, 18, 0), "2012-01-01 10:00 -0800".to_time(:utc) + assert_equal Time.local(2012, 1, 1, 10, 0), "2012-01-01 10:00 -0500".to_time + assert_equal Time.utc(2012, 1, 1, 15, 0), "2012-01-01 10:00 -0500".to_time(:utc) + assert_equal Time.local(2012, 1, 1, 5, 0), "2012-01-01 10:00 UTC".to_time + assert_equal Time.utc(2012, 1, 1, 10, 0), "2012-01-01 10:00 UTC".to_time(:utc) + assert_equal Time.local(2012, 1, 1, 13, 0), "2012-01-01 10:00 PST".to_time + assert_equal Time.utc(2012, 1, 1, 18, 0), "2012-01-01 10:00 PST".to_time(:utc) + assert_equal Time.local(2012, 1, 1, 10, 0), "2012-01-01 10:00 EST".to_time + assert_equal Time.utc(2012, 1, 1, 15, 0), "2012-01-01 10:00 EST".to_time(:utc) + end + end + + def test_daylight_savings_string_to_time_when_current_time_is_standard_time + with_env_tz "US/Eastern" do + Time.stubs(:now).returns(Time.local(2012, 1, 1)) + assert_equal Time.local(2012, 7, 1, 10, 0), "2012-07-01 10:00".to_time + assert_equal Time.utc(2012, 7, 1, 10, 0), "2012-07-01 10:00".to_time(:utc) + assert_equal Time.local(2012, 7, 1, 13, 0), "2012-07-01 10:00 -0700".to_time + assert_equal Time.utc(2012, 7, 1, 17, 0), "2012-07-01 10:00 -0700".to_time(:utc) + assert_equal Time.local(2012, 7, 1, 10, 0), "2012-07-01 10:00 -0400".to_time + assert_equal Time.utc(2012, 7, 1, 14, 0), "2012-07-01 10:00 -0400".to_time(:utc) + assert_equal Time.local(2012, 7, 1, 6, 0), "2012-07-01 10:00 UTC".to_time + assert_equal Time.utc(2012, 7, 1, 10, 0), "2012-07-01 10:00 UTC".to_time(:utc) + assert_equal Time.local(2012, 7, 1, 13, 0), "2012-07-01 10:00 PDT".to_time + assert_equal Time.utc(2012, 7, 1, 17, 0), "2012-07-01 10:00 PDT".to_time(:utc) + assert_equal Time.local(2012, 7, 1, 10, 0), "2012-07-01 10:00 EDT".to_time + assert_equal Time.utc(2012, 7, 1, 14, 0), "2012-07-01 10:00 EDT".to_time(:utc) + end + end + + def test_daylight_savings_string_to_time_when_current_time_is_daylight_savings + with_env_tz "US/Eastern" do + Time.stubs(:now).returns(Time.local(2012, 7, 1)) + assert_equal Time.local(2012, 7, 1, 10, 0), "2012-07-01 10:00".to_time + assert_equal Time.utc(2012, 7, 1, 10, 0), "2012-07-01 10:00".to_time(:utc) + assert_equal Time.local(2012, 7, 1, 13, 0), "2012-07-01 10:00 -0700".to_time + assert_equal Time.utc(2012, 7, 1, 17, 0), "2012-07-01 10:00 -0700".to_time(:utc) + assert_equal Time.local(2012, 7, 1, 10, 0), "2012-07-01 10:00 -0400".to_time + assert_equal Time.utc(2012, 7, 1, 14, 0), "2012-07-01 10:00 -0400".to_time(:utc) + assert_equal Time.local(2012, 7, 1, 6, 0), "2012-07-01 10:00 UTC".to_time + assert_equal Time.utc(2012, 7, 1, 10, 0), "2012-07-01 10:00 UTC".to_time(:utc) + assert_equal Time.local(2012, 7, 1, 13, 0), "2012-07-01 10:00 PDT".to_time + assert_equal Time.utc(2012, 7, 1, 17, 0), "2012-07-01 10:00 PDT".to_time(:utc) + assert_equal Time.local(2012, 7, 1, 10, 0), "2012-07-01 10:00 EDT".to_time + assert_equal Time.utc(2012, 7, 1, 14, 0), "2012-07-01 10:00 EDT".to_time(:utc) + end + end + + def test_partial_string_to_time_when_current_time_is_standard_time + with_env_tz "US/Eastern" do + Time.stubs(:now).returns(Time.local(2012, 1, 1)) + assert_equal Time.local(2012, 1, 1, 10, 0), "10:00".to_time + assert_equal Time.utc(2012, 1, 1, 10, 0), "10:00".to_time(:utc) + assert_equal Time.local(2012, 1, 1, 6, 0), "10:00 -0100".to_time + assert_equal Time.utc(2012, 1, 1, 11, 0), "10:00 -0100".to_time(:utc) + assert_equal Time.local(2012, 1, 1, 10, 0), "10:00 -0500".to_time + assert_equal Time.utc(2012, 1, 1, 15, 0), "10:00 -0500".to_time(:utc) + assert_equal Time.local(2012, 1, 1, 5, 0), "10:00 UTC".to_time + assert_equal Time.utc(2012, 1, 1, 10, 0), "10:00 UTC".to_time(:utc) + assert_equal Time.local(2012, 1, 1, 13, 0), "10:00 PST".to_time + assert_equal Time.utc(2012, 1, 1, 18, 0), "10:00 PST".to_time(:utc) + assert_equal Time.local(2012, 1, 1, 12, 0), "10:00 PDT".to_time + assert_equal Time.utc(2012, 1, 1, 17, 0), "10:00 PDT".to_time(:utc) + assert_equal Time.local(2012, 1, 1, 10, 0), "10:00 EST".to_time + assert_equal Time.utc(2012, 1, 1, 15, 0), "10:00 EST".to_time(:utc) + assert_equal Time.local(2012, 1, 1, 9, 0), "10:00 EDT".to_time + assert_equal Time.utc(2012, 1, 1, 14, 0), "10:00 EDT".to_time(:utc) + end + end + + def test_partial_string_to_time_when_current_time_is_daylight_savings + with_env_tz "US/Eastern" do + Time.stubs(:now).returns(Time.local(2012, 7, 1)) + assert_equal Time.local(2012, 7, 1, 10, 0), "10:00".to_time + assert_equal Time.utc(2012, 7, 1, 10, 0), "10:00".to_time(:utc) + assert_equal Time.local(2012, 7, 1, 7, 0), "10:00 -0100".to_time + assert_equal Time.utc(2012, 7, 1, 11, 0), "10:00 -0100".to_time(:utc) + assert_equal Time.local(2012, 7, 1, 11, 0), "10:00 -0500".to_time + assert_equal Time.utc(2012, 7, 1, 15, 0), "10:00 -0500".to_time(:utc) + assert_equal Time.local(2012, 7, 1, 6, 0), "10:00 UTC".to_time + assert_equal Time.utc(2012, 7, 1, 10, 0), "10:00 UTC".to_time(:utc) + assert_equal Time.local(2012, 7, 1, 14, 0), "10:00 PST".to_time + assert_equal Time.utc(2012, 7, 1, 18, 0), "10:00 PST".to_time(:utc) + assert_equal Time.local(2012, 7, 1, 13, 0), "10:00 PDT".to_time + assert_equal Time.utc(2012, 7, 1, 17, 0), "10:00 PDT".to_time(:utc) + assert_equal Time.local(2012, 7, 1, 11, 0), "10:00 EST".to_time + assert_equal Time.utc(2012, 7, 1, 15, 0), "10:00 EST".to_time(:utc) + assert_equal Time.local(2012, 7, 1, 10, 0), "10:00 EDT".to_time + assert_equal Time.utc(2012, 7, 1, 14, 0), "10:00 EDT".to_time(:utc) + end + end + + def test_string_to_datetime + assert_equal DateTime.civil(2039, 2, 27, 23, 50), "2039-02-27 23:50".to_datetime + assert_equal 0, "2039-02-27 23:50".to_datetime.offset # use UTC offset + assert_equal ::Date::ITALY, "2039-02-27 23:50".to_datetime.start # use Ruby's default start value + assert_equal DateTime.civil(2039, 2, 27, 23, 50, 19 + Rational(275038, 1000000), "-04:00"), "2039-02-27T23:50:19.275038-04:00".to_datetime + assert_nil "".to_datetime + end + + def test_partial_string_to_datetime + now = DateTime.now + assert_equal DateTime.civil(now.year, now.month, now.day, 23, 50), "23:50".to_datetime + assert_equal DateTime.civil(now.year, now.month, now.day, 23, 50, 0, "-04:00"), "23:50 -0400".to_datetime + end + + def test_string_to_date + assert_equal Date.new(2005, 2, 27), "2005-02-27".to_date + assert_nil "".to_date + assert_equal Date.new(Date.today.year, 2, 3), "Feb 3rd".to_date + end + + protected + def with_env_tz(new_tz = 'US/Eastern') + old_tz, ENV['TZ'] = ENV['TZ'], new_tz + yield + ensure + old_tz ? ENV['TZ'] = old_tz : ENV.delete('TZ') + end +end + class StringBehaviourTest < ActiveSupport::TestCase def test_acts_like_string assert 'Bambi'.acts_like_string? @@ -315,22 +476,24 @@ class StringBehaviourTest < ActiveSupport::TestCase end class CoreExtStringMultibyteTest < ActiveSupport::TestCase - UNICODE_STRING = 'こにちわ' - ASCII_STRING = 'ohayo' - BYTE_STRING = "\270\236\010\210\245" + UTF8_STRING = 'こにちわ' + ASCII_STRING = 'ohayo'.encode('US-ASCII') + EUC_JP_STRING = 'さよなら'.encode('EUC-JP') + INVALID_UTF8_STRING = "\270\236\010\210\245" def test_core_ext_adds_mb_chars - assert_respond_to UNICODE_STRING, :mb_chars + assert_respond_to UTF8_STRING, :mb_chars end def test_string_should_recognize_utf8_strings - assert UNICODE_STRING.is_utf8? + assert UTF8_STRING.is_utf8? assert ASCII_STRING.is_utf8? - assert !BYTE_STRING.is_utf8? + assert !EUC_JP_STRING.is_utf8? + assert !INVALID_UTF8_STRING.is_utf8? end def test_mb_chars_returns_instance_of_proxy_class - assert_kind_of ActiveSupport::Multibyte.proxy_class, UNICODE_STRING.mb_chars + assert_kind_of ActiveSupport::Multibyte.proxy_class, UTF8_STRING.mb_chars end end @@ -486,12 +649,6 @@ class OutputSafetyTest < ActiveSupport::TestCase assert_equal 'foo'.to_yaml, 'foo'.html_safe.to_yaml(:foo => 1) end - test 'knows whether it is encoding aware' do - assert_deprecated do - assert 'ruby'.encoding_aware? - end - end - test "call to_param returns a normal string" do string = @string.html_safe assert string.html_safe? diff --git a/activesupport/test/core_ext/thread_test.rb b/activesupport/test/core_ext/thread_test.rb new file mode 100644 index 0000000000..6a7c6e0604 --- /dev/null +++ b/activesupport/test/core_ext/thread_test.rb @@ -0,0 +1,75 @@ +require 'abstract_unit' +require 'active_support/core_ext/thread' + +class ThreadExt < ActiveSupport::TestCase + def test_main_thread_variable_in_enumerator + assert_equal Thread.main, Thread.current + + Thread.current.thread_variable_set :foo, "bar" + + thread, value = Fiber.new { + Fiber.yield [Thread.current, Thread.current.thread_variable_get(:foo)] + }.resume + + assert_equal Thread.current, thread + assert_equal Thread.current.thread_variable_get(:foo), value + end + + def test_thread_variable_in_enumerator + Thread.new { + Thread.current.thread_variable_set :foo, "bar" + + thread, value = Fiber.new { + Fiber.yield [Thread.current, Thread.current.thread_variable_get(:foo)] + }.resume + + assert_equal Thread.current, thread + assert_equal Thread.current.thread_variable_get(:foo), value + }.join + end + + def test_thread_variables + assert_equal [], Thread.new { Thread.current.thread_variables }.join.value + + t = Thread.new { + Thread.current.thread_variable_set(:foo, "bar") + Thread.current.thread_variables + } + assert_equal [:foo], t.join.value + end + + def test_thread_variable? + assert_not Thread.new { Thread.current.thread_variable?("foo") }.join.value + t = Thread.new { + Thread.current.thread_variable_set("foo", "bar") + }.join + + assert t.thread_variable?("foo") + assert t.thread_variable?(:foo) + assert_not t.thread_variable?(:bar) + end + + def test_thread_variable_strings_and_symbols_are_the_same_key + t = Thread.new {}.join + t.thread_variable_set("foo", "bar") + assert_equal "bar", t.thread_variable_get(:foo) + end + + def test_thread_variable_frozen + t = Thread.new { }.join + t.freeze + assert_raises(RuntimeError) do + t.thread_variable_set(:foo, "bar") + end + end + + def test_thread_variable_frozen_after_set + t = Thread.new { }.join + t.thread_variable_set :foo, "bar" + t.freeze + assert_raises(RuntimeError) do + t.thread_variable_set(:baz, "qux") + end + end + +end diff --git a/activesupport/test/core_ext/time_ext_test.rb b/activesupport/test/core_ext/time_ext_test.rb index 0e75104fc6..41a1df084e 100644 --- a/activesupport/test/core_ext/time_ext_test.rb +++ b/activesupport/test/core_ext/time_ext_test.rb @@ -117,10 +117,26 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase end end + def test_middle_of_day + assert_equal Time.local(2005,2,4,12,0,0), Time.local(2005,2,4,10,10,10).middle_of_day + with_env_tz 'US/Eastern' do + assert_equal Time.local(2006,4,2,12,0,0), Time.local(2006,4,2,10,10,10).middle_of_day, 'start DST' + assert_equal Time.local(2006,10,29,12,0,0), Time.local(2006,10,29,10,10,10).middle_of_day, 'ends DST' + end + with_env_tz 'NZ' do + assert_equal Time.local(2006,3,19,12,0,0), Time.local(2006,3,19,10,10,10).middle_of_day, 'ends DST' + assert_equal Time.local(2006,10,1,12,0,0), Time.local(2006,10,1,10,10,10).middle_of_day, 'start DST' + end + end + def test_beginning_of_hour assert_equal Time.local(2005,2,4,19,0,0), Time.local(2005,2,4,19,30,10).beginning_of_hour end + def test_beginning_of_minute + assert_equal Time.local(2005,2,4,19,30,0), Time.local(2005,2,4,19,30,10).beginning_of_minute + end + def test_end_of_day assert_equal Time.local(2007,8,12,23,59,59,Rational(999999999, 1000)), Time.local(2007,8,12,10,10,10).end_of_day with_env_tz 'US/Eastern' do @@ -137,6 +153,10 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase assert_equal Time.local(2005,2,4,19,59,59,Rational(999999999, 1000)), Time.local(2005,2,4,19,30,10).end_of_hour end + def test_end_of_minute + assert_equal Time.local(2005,2,4,19,30,59,Rational(999999999, 1000)), Time.local(2005,2,4,19,30,10).end_of_minute + end + def test_last_year assert_equal Time.local(2004,6,5,10), Time.local(2005,6,5,10,0,0).last_year end @@ -501,6 +521,9 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase with_env_tz "US/Central" do assert_equal "Thu, 05 Feb 2009 14:30:05 -0600", Time.local(2009, 2, 5, 14, 30, 5).to_s(:rfc822) assert_equal "Mon, 09 Jun 2008 04:05:01 -0500", Time.local(2008, 6, 9, 4, 5, 1).to_s(:rfc822) + assert_equal "2009-02-05T14:30:05-06:00", Time.local(2009, 2, 5, 14, 30, 5).to_s(:iso8601) + assert_equal "2008-06-09T04:05:01-05:00", Time.local(2008, 6, 9, 4, 5, 1).to_s(:iso8601) + assert_equal "2009-02-05T14:30:05Z", Time.utc(2009, 2, 5, 14, 30, 5).to_s(:iso8601) end end @@ -526,7 +549,11 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase end def test_to_time - assert_equal Time.local(2005, 2, 21, 17, 44, 30), Time.local(2005, 2, 21, 17, 44, 30).to_time + with_env_tz 'US/Eastern' do + assert_equal Time, Time.local(2005, 2, 21, 17, 44, 30).to_time.class + assert_equal Time.local(2005, 2, 21, 17, 44, 30), Time.local(2005, 2, 21, 17, 44, 30).to_time + assert_equal Time.local(2005, 2, 21, 17, 44, 30).utc_offset, Time.local(2005, 2, 21, 17, 44, 30).to_time.utc_offset + end end # NOTE: this test seems to fail (changeset 1958) only on certain platforms, @@ -566,46 +593,6 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase assert_equal 29, Time.days_in_month(2) end - def test_time_with_datetime_fallback - assert_equal Time.time_with_datetime_fallback(:utc, 2005, 2, 21, 17, 44, 30), Time.utc(2005, 2, 21, 17, 44, 30) - assert_equal Time.time_with_datetime_fallback(:local, 2005, 2, 21, 17, 44, 30), Time.local(2005, 2, 21, 17, 44, 30) - assert_equal Time.time_with_datetime_fallback(:utc, 2039, 2, 21, 17, 44, 30), DateTime.civil(2039, 2, 21, 17, 44, 30, 0) - assert_equal Time.time_with_datetime_fallback(:local, 2039, 2, 21, 17, 44, 30), DateTime.civil_from_format(:local, 2039, 2, 21, 17, 44, 30) - assert_equal Time.time_with_datetime_fallback(:utc, 1900, 2, 21, 17, 44, 30), DateTime.civil(1900, 2, 21, 17, 44, 30, 0) - assert_equal Time.time_with_datetime_fallback(:utc, 2005), Time.utc(2005) - assert_equal Time.time_with_datetime_fallback(:utc, 2039), DateTime.civil(2039, 1, 1, 0, 0, 0, 0) - assert_equal Time.time_with_datetime_fallback(:utc, 2005, 2, 21, 17, 44, 30, 1), Time.utc(2005, 2, 21, 17, 44, 30, 1) #with usec - # This won't overflow on 64bit linux - unless time_is_64bits? - assert_equal Time.time_with_datetime_fallback(:local, 1900, 2, 21, 17, 44, 30), DateTime.civil_from_format(:local, 1900, 2, 21, 17, 44, 30) - assert_equal Time.time_with_datetime_fallback(:utc, 2039, 2, 21, 17, 44, 30, 1), - DateTime.civil(2039, 2, 21, 17, 44, 30, 0, 0) - assert_equal ::Date::ITALY, Time.time_with_datetime_fallback(:utc, 2039, 2, 21, 17, 44, 30, 1).start # use Ruby's default start value - end - silence_warnings do - 0.upto(138) do |year| - [:utc, :local].each do |format| - assert_equal year, Time.time_with_datetime_fallback(format, year).year - end - end - end - end - - def test_utc_time - assert_equal Time.utc_time(2005, 2, 21, 17, 44, 30), Time.utc(2005, 2, 21, 17, 44, 30) - assert_equal Time.utc_time(2039, 2, 21, 17, 44, 30), DateTime.civil(2039, 2, 21, 17, 44, 30, 0) - assert_equal Time.utc_time(1901, 2, 21, 17, 44, 30), DateTime.civil(1901, 2, 21, 17, 44, 30, 0) - end - - def test_local_time - assert_equal Time.local_time(2005, 2, 21, 17, 44, 30), Time.local(2005, 2, 21, 17, 44, 30) - assert_equal Time.local_time(2039, 2, 21, 17, 44, 30), DateTime.civil_from_format(:local, 2039, 2, 21, 17, 44, 30) - - unless time_is_64bits? - assert_equal Time.local_time(1901, 2, 21, 17, 44, 30), DateTime.civil_from_format(:local, 1901, 2, 21, 17, 44, 30) - end - end - def test_last_month_on_31st assert_equal Time.local(2004, 2, 29), Time.local(2004, 3, 31).last_month end @@ -717,6 +704,82 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase assert_equal(-1, Time.utc(2000) <=> ActiveSupport::TimeWithZone.new( Time.utc(2000, 1, 1, 0, 0, 1), ActiveSupport::TimeZone['UTC'] )) end + def test_at_with_datetime + assert_equal Time.utc(2000, 1, 1, 0, 0, 0), Time.at(DateTime.civil(2000, 1, 1, 0, 0, 0)) + + # Only test this if the underlying Time.at raises a TypeError + begin + Time.at_without_coercion(Time.now, 0) + rescue TypeError + assert_raise(TypeError) { assert_equal(Time.utc(2000, 1, 1, 0, 0, 0), Time.at(DateTime.civil(2000, 1, 1, 0, 0, 0), 0)) } + end + end + + def test_at_with_datetime_returns_local_time + with_env_tz 'US/Eastern' do + dt = DateTime.civil(2000, 1, 1, 0, 0, 0, '+0') + assert_equal Time.local(1999, 12, 31, 19, 0, 0), Time.at(dt) + assert_equal 'EST', Time.at(dt).zone + assert_equal(-18000, Time.at(dt).utc_offset) + + # Daylight savings + dt = DateTime.civil(2000, 7, 1, 1, 0, 0, '+1') + assert_equal Time.local(2000, 6, 30, 20, 0, 0), Time.at(dt) + assert_equal 'EDT', Time.at(dt).zone + assert_equal(-14400, Time.at(dt).utc_offset) + end + end + + def test_at_with_time_with_zone + assert_equal Time.utc(2000, 1, 1, 0, 0, 0), Time.at(ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 1, 0, 0, 0), ActiveSupport::TimeZone['UTC'])) + + # Only test this if the underlying Time.at raises a TypeError + begin + Time.at_without_coercion(Time.now, 0) + rescue TypeError + assert_raise(TypeError) { assert_equal(Time.utc(2000, 1, 1, 0, 0, 0), Time.at(ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 1, 0, 0, 0), ActiveSupport::TimeZone['UTC']), 0)) } + end + end + + def test_at_with_time_with_zone_returns_local_time + with_env_tz 'US/Eastern' do + twz = ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 1, 0, 0, 0), ActiveSupport::TimeZone['London']) + assert_equal Time.local(1999, 12, 31, 19, 0, 0), Time.at(twz) + assert_equal 'EST', Time.at(twz).zone + assert_equal(-18000, Time.at(twz).utc_offset) + + # Daylight savings + twz = ActiveSupport::TimeWithZone.new(Time.utc(2000, 7, 1, 0, 0, 0), ActiveSupport::TimeZone['London']) + assert_equal Time.local(2000, 6, 30, 20, 0, 0), Time.at(twz) + assert_equal 'EDT', Time.at(twz).zone + assert_equal(-14400, Time.at(twz).utc_offset) + end + end + + def test_at_with_time_microsecond_precision + assert_equal Time.at(Time.utc(2000, 1, 1, 0, 0, 0, 111)).to_f, Time.utc(2000, 1, 1, 0, 0, 0, 111).to_f + end + + def test_at_with_utc_time + with_env_tz 'US/Eastern' do + assert_equal Time.utc(2000), Time.at(Time.utc(2000)) + assert_equal 'UTC', Time.at(Time.utc(2000)).zone + assert_equal(0, Time.at(Time.utc(2000)).utc_offset) + end + end + + def test_at_with_local_time + with_env_tz 'US/Eastern' do + assert_equal Time.local(2000), Time.at(Time.local(2000)) + assert_equal 'EST', Time.at(Time.local(2000)).zone + assert_equal(-18000, Time.at(Time.local(2000)).utc_offset) + + assert_equal Time.local(2000, 7, 1), Time.at(Time.local(2000, 7, 1)) + assert_equal 'EDT', Time.at(Time.local(2000, 7, 1)).zone + assert_equal(-14400, Time.at(Time.local(2000, 7, 1)).utc_offset) + end + end + def test_eql? assert_equal true, Time.utc(2000).eql?( ActiveSupport::TimeWithZone.new(Time.utc(2000), ActiveSupport::TimeZone['UTC']) ) assert_equal true, Time.utc(2000).eql?( ActiveSupport::TimeWithZone.new(Time.utc(2000), ActiveSupport::TimeZone["Hawaii"]) ) @@ -786,9 +849,6 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase old_tz ? ENV['TZ'] = old_tz : ENV.delete('TZ') end - def time_is_64bits? - Time.time_with_datetime_fallback(:utc, 2039, 2, 21, 17, 44, 30, 1).is_a?(Time) - end end class TimeExtMarshalingTest < ActiveSupport::TestCase diff --git a/activesupport/test/core_ext/time_with_zone_test.rb b/activesupport/test/core_ext/time_with_zone_test.rb index 1293f104e5..5494824a40 100644 --- a/activesupport/test/core_ext/time_with_zone_test.rb +++ b/activesupport/test/core_ext/time_with_zone_test.rb @@ -75,11 +75,16 @@ class TimeWithZoneTest < ActiveSupport::TestCase def test_to_json_with_use_standard_json_time_format_config_set_to_true old, ActiveSupport.use_standard_json_time_format = ActiveSupport.use_standard_json_time_format, true - assert_equal "\"1999-12-31T19:00:00-05:00\"", ActiveSupport::JSON.encode(@twz) + assert_equal "\"1999-12-31T19:00:00.000-05:00\"", ActiveSupport::JSON.encode(@twz) ensure ActiveSupport.use_standard_json_time_format = old end + def test_to_json_when_wrapping_a_date_time + twz = ActiveSupport::TimeWithZone.new(DateTime.civil(2000), @time_zone) + assert_equal '"1999-12-31T19:00:00.000-05:00"', ActiveSupport::JSON.encode(twz) + end + def test_nsec local = Time.local(2011,6,7,23,59,59,Rational(999999999, 1000)) with_zone = ActiveSupport::TimeWithZone.new(nil, ActiveSupport::TimeZone["Hawaii"], local) @@ -326,8 +331,23 @@ class TimeWithZoneTest < ActiveSupport::TestCase assert_equal 946684800, twz.to_i end + def test_to_r + result = ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 1), ActiveSupport::TimeZone['Hawaii']).to_r + assert_equal Rational(946684800, 1), result + assert_kind_of Rational, result + end + + def test_time_at + time = ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 1), ActiveSupport::TimeZone['Hawaii']) + assert_equal time, Time.at(time) + end + def test_to_time - assert_equal @twz, @twz.to_time + with_env_tz 'US/Eastern' do + assert_equal Time, @twz.to_time.class + assert_equal Time.local(1999, 12, 31, 19), @twz.to_time + assert_equal Time.local(1999, 12, 31, 19).utc_offset, @twz.to_time.utc_offset + end end def test_to_date @@ -430,6 +450,16 @@ class TimeWithZoneTest < ActiveSupport::TestCase assert_equal 0, twz.usec end + def test_usec_returns_sec_fraction_when_datetime_is_wrapped + twz = ActiveSupport::TimeWithZone.new(DateTime.civil(2000, 1, 1, 0, 0, Rational(1,2)), @time_zone) + assert_equal 500000, twz.usec + end + + def test_nsec_returns_sec_fraction_when_datetime_is_wrapped + twz = ActiveSupport::TimeWithZone.new(DateTime.civil(2000, 1, 1, 0, 0, Rational(1,2)), @time_zone) + assert_equal 500000000, twz.nsec + end + def test_utc_to_local_conversion_saves_period_in_instance_variable assert_nil @twz.instance_variable_get('@period') @twz.time @@ -535,6 +565,20 @@ class TimeWithZoneTest < ActiveSupport::TestCase assert_equal "Fri, 31 Dec 1999 19:59:59 EST -05:00", twz.end_of_hour.inspect end + def test_beginning_of_minute + utc = Time.utc(2000, 1, 1, 0, 30, 10) + twz = ActiveSupport::TimeWithZone.new(utc, @time_zone) + assert_equal "Fri, 31 Dec 1999 19:30:10 EST -05:00", twz.inspect + assert_equal "Fri, 31 Dec 1999 19:00:00 EST -05:00", twz.beginning_of_hour.inspect + end + + def test_end_of_minute + utc = Time.utc(2000, 1, 1, 0, 30, 10) + twz = ActiveSupport::TimeWithZone.new(utc, @time_zone) + assert_equal "Fri, 31 Dec 1999 19:30:10 EST -05:00", twz.inspect + assert_equal "Fri, 31 Dec 1999 19:30:59 EST -05:00", twz.end_of_minute.inspect + end + def test_since assert_equal "Fri, 31 Dec 1999 19:00:01 EST -05:00", @twz.since(1).inspect end @@ -750,6 +794,14 @@ class TimeWithZoneTest < ActiveSupport::TestCase assert_equal "Sun, 15 Jul 2007 10:30:00 EDT -04:00", (twz - 1.year).inspect end + def test_no_method_error_has_proper_context + e = assert_raises(NoMethodError) { + @twz.this_method_does_not_exist + } + assert_equal "undefined method `this_method_does_not_exist' for Fri, 31 Dec 1999 19:00:00 EST -05:00:Time", e.message + assert_no_match "rescue", e.backtrace.first + end + protected def with_env_tz(new_tz = 'US/Eastern') old_tz, ENV['TZ'] = ENV['TZ'], new_tz @@ -945,6 +997,15 @@ class TimeWithZoneMethodsForTimeAndDateTimeTest < ActiveSupport::TestCase Time.zone = nil end + def test_time_in_time_zone_doesnt_affect_receiver + with_env_tz 'Europe/London' do + time = Time.local(2000, 7, 1) + time_with_zone = time.in_time_zone('Eastern Time (US & Canada)') + assert_equal Time.utc(2000, 6, 30, 23, 0, 0), time_with_zone + assert_not time.utc?, 'time expected to be local, but is UTC' + end + end + protected def with_env_tz(new_tz = 'US/Eastern') old_tz, ENV['TZ'] = ENV['TZ'], new_tz @@ -953,3 +1014,131 @@ class TimeWithZoneMethodsForTimeAndDateTimeTest < ActiveSupport::TestCase old_tz ? ENV['TZ'] = old_tz : ENV.delete('TZ') end end + +class TimeWithZoneMethodsForDate < ActiveSupport::TestCase + def setup + @d = Date.civil(2000) + end + + def teardown + Time.zone = nil + end + + def test_in_time_zone + with_tz_default 'Alaska' do + assert_equal 'Sat, 01 Jan 2000 00:00:00 AKST -09:00', @d.in_time_zone.inspect + end + with_tz_default 'Hawaii' do + assert_equal 'Sat, 01 Jan 2000 00:00:00 HST -10:00', @d.in_time_zone.inspect + end + with_tz_default nil do + assert_equal @d.to_time, @d.in_time_zone + end + end + + def test_nil_time_zone + with_tz_default nil do + assert !@d.in_time_zone.respond_to?(:period), 'no period method' + end + end + + def test_in_time_zone_with_argument + with_tz_default 'Eastern Time (US & Canada)' do # Time.zone will not affect #in_time_zone(zone) + assert_equal 'Sat, 01 Jan 2000 00:00:00 AKST -09:00', @d.in_time_zone('Alaska').inspect + assert_equal 'Sat, 01 Jan 2000 00:00:00 HST -10:00', @d.in_time_zone('Hawaii').inspect + assert_equal 'Sat, 01 Jan 2000 00:00:00 UTC +00:00', @d.in_time_zone('UTC').inspect + assert_equal 'Sat, 01 Jan 2000 00:00:00 AKST -09:00', @d.in_time_zone(-9.hours).inspect + end + end + + def test_in_time_zone_with_invalid_argument + assert_raise(ArgumentError) { @d.in_time_zone("No such timezone exists") } + assert_raise(ArgumentError) { @d.in_time_zone(-15.hours) } + assert_raise(ArgumentError) { @d.in_time_zone(Object.new) } + end + + protected + def with_tz_default(tz = nil) + old_tz = Time.zone + Time.zone = tz + yield + ensure + Time.zone = old_tz + end +end + +class TimeWithZoneMethodsForString < ActiveSupport::TestCase + def setup + @s = "Sat, 01 Jan 2000 00:00:00" + @u = "Sat, 01 Jan 2000 00:00:00 UTC +00:00" + @z = "Fri, 31 Dec 1999 19:00:00 EST -05:00" + end + + def teardown + Time.zone = nil + end + + def test_in_time_zone + with_tz_default 'Alaska' do + assert_equal 'Sat, 01 Jan 2000 00:00:00 AKST -09:00', @s.in_time_zone.inspect + assert_equal 'Fri, 31 Dec 1999 15:00:00 AKST -09:00', @u.in_time_zone.inspect + assert_equal 'Fri, 31 Dec 1999 15:00:00 AKST -09:00', @z.in_time_zone.inspect + end + with_tz_default 'Hawaii' do + assert_equal 'Sat, 01 Jan 2000 00:00:00 HST -10:00', @s.in_time_zone.inspect + assert_equal 'Fri, 31 Dec 1999 14:00:00 HST -10:00', @u.in_time_zone.inspect + assert_equal 'Fri, 31 Dec 1999 14:00:00 HST -10:00', @z.in_time_zone.inspect + end + with_tz_default nil do + assert_equal @s.to_time, @s.in_time_zone + assert_equal @u.to_time, @u.in_time_zone + assert_equal @z.to_time, @z.in_time_zone + end + end + + 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' + end + end + + def test_in_time_zone_with_argument + with_tz_default 'Eastern Time (US & Canada)' do # Time.zone will not affect #in_time_zone(zone) + assert_equal 'Sat, 01 Jan 2000 00:00:00 AKST -09:00', @s.in_time_zone('Alaska').inspect + assert_equal 'Fri, 31 Dec 1999 15:00:00 AKST -09:00', @u.in_time_zone('Alaska').inspect + assert_equal 'Fri, 31 Dec 1999 15:00:00 AKST -09:00', @z.in_time_zone('Alaska').inspect + assert_equal 'Sat, 01 Jan 2000 00:00:00 HST -10:00', @s.in_time_zone('Hawaii').inspect + assert_equal 'Fri, 31 Dec 1999 14:00:00 HST -10:00', @u.in_time_zone('Hawaii').inspect + assert_equal 'Fri, 31 Dec 1999 14:00:00 HST -10:00', @z.in_time_zone('Hawaii').inspect + assert_equal 'Sat, 01 Jan 2000 00:00:00 UTC +00:00', @s.in_time_zone('UTC').inspect + assert_equal 'Sat, 01 Jan 2000 00:00:00 UTC +00:00', @u.in_time_zone('UTC').inspect + assert_equal 'Sat, 01 Jan 2000 00:00:00 UTC +00:00', @z.in_time_zone('UTC').inspect + assert_equal 'Sat, 01 Jan 2000 00:00:00 AKST -09:00', @s.in_time_zone(-9.hours).inspect + assert_equal 'Fri, 31 Dec 1999 15:00:00 AKST -09:00', @u.in_time_zone(-9.hours).inspect + assert_equal 'Fri, 31 Dec 1999 15:00:00 AKST -09:00', @z.in_time_zone(-9.hours).inspect + end + end + + def test_in_time_zone_with_invalid_argument + assert_raise(ArgumentError) { @s.in_time_zone("No such timezone exists") } + assert_raise(ArgumentError) { @u.in_time_zone("No such timezone exists") } + assert_raise(ArgumentError) { @z.in_time_zone("No such timezone exists") } + assert_raise(ArgumentError) { @s.in_time_zone(-15.hours) } + assert_raise(ArgumentError) { @u.in_time_zone(-15.hours) } + assert_raise(ArgumentError) { @z.in_time_zone(-15.hours) } + assert_raise(ArgumentError) { @s.in_time_zone(Object.new) } + assert_raise(ArgumentError) { @u.in_time_zone(Object.new) } + assert_raise(ArgumentError) { @z.in_time_zone(Object.new) } + end + + protected + def with_tz_default(tz = nil) + old_tz = Time.zone + Time.zone = tz + yield + ensure + Time.zone = old_tz + end +end diff --git a/activesupport/test/dependencies/raises_exception_without_blame_file.rb b/activesupport/test/dependencies/raises_exception_without_blame_file.rb new file mode 100644 index 0000000000..4b2da6ff30 --- /dev/null +++ b/activesupport/test/dependencies/raises_exception_without_blame_file.rb @@ -0,0 +1,5 @@ +exception = Exception.new('I am not blamable!') +class << exception + undef_method(:blame_file!) +end +raise exception diff --git a/activesupport/test/dependencies_test.rb b/activesupport/test/dependencies_test.rb index 51a7e4b2fe..2392b71960 100644 --- a/activesupport/test/dependencies_test.rb +++ b/activesupport/test/dependencies_test.rb @@ -1,7 +1,7 @@ require 'abstract_unit' require 'pp' require 'active_support/dependencies' -require 'dependecies_test_helpers' +require 'dependencies_test_helpers' module ModuleWithMissing mattr_accessor :missing_count @@ -20,7 +20,7 @@ class DependenciesTest < ActiveSupport::TestCase ActiveSupport::Dependencies.clear end - include DependeciesTestHelpers + include DependenciesTestHelpers def test_depend_on_path skip "LoadError#path does not exist" if RUBY_VERSION < '2.0.0' @@ -35,6 +35,17 @@ class DependenciesTest < ActiveSupport::TestCase assert_equal expected.path, e.path end + def test_require_dependency_accepts_an_object_which_implements_to_path + o = Object.new + def o.to_path; 'dependencies/service_one'; end + assert_nothing_raised { + require_dependency o + } + assert defined?(ServiceOne) + ensure + remove_constants(:ServiceOne) + end + def test_tracking_loaded_files require_dependency 'dependencies/service_one' require_dependency 'dependencies/service_two' @@ -76,6 +87,14 @@ class DependenciesTest < ActiveSupport::TestCase end end + def test_dependency_which_raises_doesnt_blindly_call_blame_file! + with_loading do + filename = 'dependencies/raises_exception_without_blame_file' + + assert_raises(Exception) { require_dependency filename } + end + end + def test_warnings_should_be_enabled_on_first_load with_loading 'dependencies' do old_warnings, ActiveSupport::Dependencies.warnings_on_first_load = ActiveSupport::Dependencies.warnings_on_first_load, true @@ -363,12 +382,10 @@ class DependenciesTest < ActiveSupport::TestCase end def test_smart_name_error_strings - begin - Object.module_eval "ImaginaryObject" - flunk "No raise!!" - rescue NameError => e - assert e.message.include?("uninitialized constant ImaginaryObject") - end + Object.module_eval "ImaginaryObject" + flunk "No raise!!" + rescue NameError => e + assert e.message.include?("uninitialized constant ImaginaryObject") end def test_loadable_constants_for_path_should_handle_empty_autoloads @@ -528,7 +545,6 @@ class DependenciesTest < ActiveSupport::TestCase m = Module.new m.module_eval "def a() CountingLoader; end" extend m - kls = nil with_autoloading_fixtures do kls = nil assert_nothing_raised { kls = a } @@ -642,6 +658,14 @@ class DependenciesTest < ActiveSupport::TestCase Object.class_eval { remove_const :E } end + def test_constants_in_capitalized_nesting_marked_as_autoloaded + with_autoloading_fixtures do + ActiveSupport::Dependencies.load_missing_constant(HTML, "SomeClass") + + assert ActiveSupport::Dependencies.autoloaded?("HTML::SomeClass") + end + end + def test_unloadable with_autoloading_fixtures do Object.const_set :M, Module.new @@ -880,7 +904,7 @@ class DependenciesTest < ActiveSupport::TestCase def test_autoload_doesnt_shadow_name_error with_autoloading_fixtures do Object.send(:remove_const, :RaisesNameError) if defined?(::RaisesNameError) - 2.times do |i| + 2.times do begin ::RaisesNameError::FooBarBaz.object_id flunk 'should have raised NameError when autoloaded file referenced FooBarBaz' @@ -923,10 +947,8 @@ class DependenciesTest < ActiveSupport::TestCase def test_remove_constant_does_not_autoload_already_removed_parents_as_a_side_effect with_autoloading_fixtures do - silence_warnings do - ::A - ::A::B - end + _ = ::A # assignment to silence parse-time warning "possibly useless use of :: in void context" + _ = ::A::B # assignment to silence parse-time warning "possibly useless use of :: in void context" ActiveSupport::Dependencies.remove_constant('A') ActiveSupport::Dependencies.remove_constant('A::B') assert !defined?(A) @@ -936,9 +958,7 @@ class DependenciesTest < ActiveSupport::TestCase def test_load_once_constants_should_not_be_unloaded with_autoloading_fixtures do ActiveSupport::Dependencies.autoload_once_paths = ActiveSupport::Dependencies.autoload_paths - silence_warnings do - ::A - end + _ = ::A # assignment to silence parse-time warning "possibly useless use of :: in void context" assert defined?(A) ActiveSupport::Dependencies.clear assert defined?(A) diff --git a/activesupport/test/dependecies_test_helpers.rb b/activesupport/test/dependencies_test_helpers.rb index 4b46d32fb4..9268512a97 100644 --- a/activesupport/test/dependecies_test_helpers.rb +++ b/activesupport/test/dependencies_test_helpers.rb @@ -1,4 +1,4 @@ -module DependeciesTestHelpers +module DependenciesTestHelpers def with_loading(*from) old_mechanism, ActiveSupport::Dependencies.mechanism = ActiveSupport::Dependencies.mechanism, :load this_dir = File.dirname(__FILE__) diff --git a/activesupport/test/deprecation_test.rb b/activesupport/test/deprecation_test.rb index 332100f5a1..9674851b9d 100644 --- a/activesupport/test/deprecation_test.rb +++ b/activesupport/test/deprecation_test.rb @@ -75,6 +75,11 @@ class DeprecationTest < ActiveSupport::TestCase end end + def test_deprecate_object + deprecated_object = ActiveSupport::Deprecation::DeprecatedObjectProxy.new(Object.new, ':bomb:') + assert_deprecated(/:bomb:/) { deprecated_object.to_s } + end + def test_nil_behavior_is_ignored ActiveSupport::Deprecation.behavior = nil assert_deprecated(/foo=nil/) { @dtc.partially } @@ -93,6 +98,22 @@ class DeprecationTest < ActiveSupport::TestCase assert_match(/foo=nil/, @b) end + def test_raise_behaviour + ActiveSupport::Deprecation.behavior = :raise + + message = 'Revise this deprecated stuff now!' + callstack = %w(foo bar baz) + + begin + ActiveSupport::Deprecation.behavior.first.call(message, callstack) + rescue ActiveSupport::DeprecationException => e + assert_equal message, e.message + assert_equal callstack, e.backtrace + else + flunk 'the :raise deprecation behaviour should raise the expected exception' + end + end + def test_default_stderr_behavior ActiveSupport::Deprecation.behavior = :stderr behavior = ActiveSupport::Deprecation.behavior.first @@ -119,9 +140,10 @@ class DeprecationTest < ActiveSupport::TestCase ActiveSupport::Deprecation.behavior = :silence behavior = ActiveSupport::Deprecation.behavior.first - assert_blank capture(:stderr) { + stderr_output = capture(:stderr) { assert_nil behavior.call('Some error!', ['call stack!']) } + assert stderr_output.blank? end def test_deprecated_instance_variable_proxy @@ -138,6 +160,7 @@ class DeprecationTest < ActiveSupport::TestCase def test_deprecated_constant_proxy assert_not_deprecated { Deprecatee::B::C } assert_deprecated('Deprecatee::A') { assert_equal Deprecatee::B::C, Deprecatee::A } + assert_not_deprecated { assert_equal Deprecatee::B::C.class, Deprecatee::A.class } end def test_assert_deprecation_without_match @@ -254,10 +277,10 @@ class DeprecationTest < ActiveSupport::TestCase klass::OLD.to_s end end - + def test_deprecated_instance_variable_with_instance_deprecator deprecator = deprecator_with_messages - + klass = Class.new() do def initialize(deprecator) @request = ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy.new(self, :request, :@request, deprecator) diff --git a/activesupport/test/descendants_tracker_with_autoloading_test.rb b/activesupport/test/descendants_tracker_with_autoloading_test.rb index ab1be296f8..a2ae066a21 100644 --- a/activesupport/test/descendants_tracker_with_autoloading_test.rb +++ b/activesupport/test/descendants_tracker_with_autoloading_test.rb @@ -6,7 +6,7 @@ require 'descendants_tracker_test_cases' class DescendantsTrackerWithAutoloadingTest < ActiveSupport::TestCase include DescendantsTrackerTestCases - def test_clear_with_autoloaded_parent_children_and_granchildren + def test_clear_with_autoloaded_parent_children_and_grandchildren mark_as_autoloaded(*ALL) do ActiveSupport::DescendantsTracker.clear ALL.each do |k| @@ -15,7 +15,7 @@ class DescendantsTrackerWithAutoloadingTest < ActiveSupport::TestCase end end - def test_clear_with_autoloaded_children_and_granchildren + def test_clear_with_autoloaded_children_and_grandchildren mark_as_autoloaded Child1, Grandchild1, Grandchild2 do ActiveSupport::DescendantsTracker.clear assert_equal_sets [Child2], Parent.descendants @@ -23,7 +23,7 @@ class DescendantsTrackerWithAutoloadingTest < ActiveSupport::TestCase end end - def test_clear_with_autoloaded_granchildren + def test_clear_with_autoloaded_grandchildren mark_as_autoloaded Grandchild1, Grandchild2 do ActiveSupport::DescendantsTracker.clear assert_equal_sets [Child1, Child2], Parent.descendants diff --git a/activesupport/test/fixtures/xml/jdom_doctype.dtd b/activesupport/test/fixtures/xml/jdom_doctype.dtd new file mode 100644 index 0000000000..89480496ef --- /dev/null +++ b/activesupport/test/fixtures/xml/jdom_doctype.dtd @@ -0,0 +1 @@ +<!ENTITY a "external entity"> diff --git a/activesupport/test/fixtures/xml/jdom_entities.txt b/activesupport/test/fixtures/xml/jdom_entities.txt new file mode 100644 index 0000000000..0337fdaa08 --- /dev/null +++ b/activesupport/test/fixtures/xml/jdom_entities.txt @@ -0,0 +1 @@ +<!ENTITY a "hello"> diff --git a/activesupport/test/fixtures/xml/jdom_include.txt b/activesupport/test/fixtures/xml/jdom_include.txt new file mode 100644 index 0000000000..239ca3afaf --- /dev/null +++ b/activesupport/test/fixtures/xml/jdom_include.txt @@ -0,0 +1 @@ +include me diff --git a/activesupport/test/gzip_test.rb b/activesupport/test/gzip_test.rb index 75a0505899..0e3cf3b429 100644 --- a/activesupport/test/gzip_test.rb +++ b/activesupport/test/gzip_test.rb @@ -4,6 +4,12 @@ require 'active_support/core_ext/object/blank' class GzipTest < ActiveSupport::TestCase def test_compress_should_decompress_to_the_same_value assert_equal "Hello World", ActiveSupport::Gzip.decompress(ActiveSupport::Gzip.compress("Hello World")) + assert_equal "Hello World", ActiveSupport::Gzip.decompress(ActiveSupport::Gzip.compress("Hello World", Zlib::NO_COMPRESSION)) + assert_equal "Hello World", ActiveSupport::Gzip.decompress(ActiveSupport::Gzip.compress("Hello World", Zlib::BEST_SPEED)) + assert_equal "Hello World", ActiveSupport::Gzip.decompress(ActiveSupport::Gzip.compress("Hello World", Zlib::BEST_COMPRESSION)) + assert_equal "Hello World", ActiveSupport::Gzip.decompress(ActiveSupport::Gzip.compress("Hello World", nil, Zlib::FILTERED)) + assert_equal "Hello World", ActiveSupport::Gzip.decompress(ActiveSupport::Gzip.compress("Hello World", nil, Zlib::HUFFMAN_ONLY)) + assert_equal "Hello World", ActiveSupport::Gzip.decompress(ActiveSupport::Gzip.compress("Hello World", nil, nil)) end def test_compress_should_return_a_binary_string @@ -12,4 +18,16 @@ class GzipTest < ActiveSupport::TestCase assert_equal Encoding.find('binary'), compressed.encoding assert !compressed.blank?, "a compressed blank string should not be blank" end + + def test_compress_should_return_gzipped_string_by_compression_level + source_string = "Hello World"*100 + + gzipped_by_speed = ActiveSupport::Gzip.compress(source_string, Zlib::BEST_SPEED) + assert_equal 1, Zlib::GzipReader.new(StringIO.new(gzipped_by_speed)).level + + gzipped_by_best_compression = ActiveSupport::Gzip.compress(source_string, Zlib::BEST_COMPRESSION) + assert_equal 9, Zlib::GzipReader.new(StringIO.new(gzipped_by_best_compression)).level + + assert_equal true, (gzipped_by_best_compression.bytesize < gzipped_by_speed.bytesize) + end end diff --git a/activesupport/test/i18n_test.rb b/activesupport/test/i18n_test.rb index ddbba444cf..5ef59b6e6b 100644 --- a/activesupport/test/i18n_test.rb +++ b/activesupport/test/i18n_test.rb @@ -62,7 +62,7 @@ class I18nTest < ActiveSupport::TestCase end def test_date_order - assert_equal [:year, :month, :day], I18n.translate(:'date.order') + assert_equal %w(year month day), I18n.translate(:'date.order') end def test_time_am diff --git a/activesupport/test/inflector_test.rb b/activesupport/test/inflector_test.rb index aa41e57928..32739d45a9 100644 --- a/activesupport/test/inflector_test.rb +++ b/activesupport/test/inflector_test.rb @@ -61,9 +61,7 @@ class InflectorTest < ActiveSupport::TestCase assert_equal(plural, ActiveSupport::Inflector.pluralize(plural)) assert_equal(plural.capitalize, ActiveSupport::Inflector.pluralize(plural.capitalize)) end - end - SingularToPlural.each do |singular, plural| define_method "test_singularize_singular_#{singular}" do assert_equal(singular, ActiveSupport::Inflector.singularize(singular)) assert_equal(singular.capitalize, ActiveSupport::Inflector.singularize(singular.capitalize)) @@ -78,9 +76,10 @@ class InflectorTest < ActiveSupport::TestCase ActiveSupport::Inflector.inflections.uncountable "series" # Return to normal end - MixtureToTitleCase.each do |before, titleized| - define_method "test_titleize_#{before}" do - assert_equal(titleized, ActiveSupport::Inflector.titleize(before)) + MixtureToTitleCase.each_with_index do |(before, titleized), index| + define_method "test_titleize_mixture_to_title_case_#{index}" do + assert_equal(titleized, ActiveSupport::Inflector.titleize(before), "mixture \ + to TitleCase failed for #{before}") end end @@ -231,25 +230,35 @@ class InflectorTest < ActiveSupport::TestCase end end +# FIXME: get following tests to pass on jruby, currently skipped +# +# Currently this fails because ActiveSupport::Multibyte::Unicode#tidy_bytes +# required a specific Encoding::Converter(UTF-8 to UTF8-MAC) which unavailable on JRuby +# causing our tests to error out. +# related bug http://jira.codehaus.org/browse/JRUBY-7194 def test_parameterize + jruby_skip "UTF-8 to UTF8-MAC Converter is unavailable" StringToParameterized.each do |some_string, parameterized_string| assert_equal(parameterized_string, ActiveSupport::Inflector.parameterize(some_string)) end end def test_parameterize_and_normalize + jruby_skip "UTF-8 to UTF8-MAC Converter is unavailable" StringToParameterizedAndNormalized.each do |some_string, parameterized_string| assert_equal(parameterized_string, ActiveSupport::Inflector.parameterize(some_string)) end end def test_parameterize_with_custom_separator + jruby_skip "UTF-8 to UTF8-MAC Converter is unavailable" StringToParameterizeWithUnderscore.each do |some_string, parameterized_string| assert_equal(parameterized_string, ActiveSupport::Inflector.parameterize(some_string, '_')) end end def test_parameterize_with_multi_character_separator + jruby_skip "UTF-8 to UTF8-MAC Converter is unavailable" StringToParameterized.each do |some_string, parameterized_string| assert_equal(parameterized_string.gsub('-', '__sep__'), ActiveSupport::Inflector.parameterize(some_string, '__sep__')) end @@ -326,7 +335,7 @@ class InflectorTest < ActiveSupport::TestCase end def test_underscore_as_reverse_of_dasherize - UnderscoresToDashes.each do |underscored, dasherized| + UnderscoresToDashes.each_key do |underscored| assert_equal(underscored, ActiveSupport::Inflector.underscore(ActiveSupport::Inflector.dasherize(underscored))) end end @@ -361,7 +370,7 @@ class InflectorTest < ActiveSupport::TestCase inflect.singular(/s$/, '') inflect.singular(/es$/, '') - + inflect.irregular('el', 'los') end @@ -421,33 +430,36 @@ class InflectorTest < ActiveSupport::TestCase end end - Irregularities.each do |irregularity| - singular, plural = *irregularity - ActiveSupport::Inflector.inflections do |inflect| - define_method("test_irregularity_between_#{singular}_and_#{plural}") do - inflect.irregular(singular, plural) - assert_equal singular, ActiveSupport::Inflector.singularize(plural) - assert_equal plural, ActiveSupport::Inflector.pluralize(singular) + Irregularities.each do |singular, plural| + define_method("test_irregularity_between_#{singular}_and_#{plural}") do + with_dup do + ActiveSupport::Inflector.inflections do |inflect| + inflect.irregular(singular, plural) + assert_equal singular, ActiveSupport::Inflector.singularize(plural) + assert_equal plural, ActiveSupport::Inflector.pluralize(singular) + end end end end - Irregularities.each do |irregularity| - singular, plural = *irregularity - ActiveSupport::Inflector.inflections do |inflect| - define_method("test_pluralize_of_irregularity_#{plural}_should_be_the_same") do - inflect.irregular(singular, plural) - assert_equal plural, ActiveSupport::Inflector.pluralize(plural) + Irregularities.each do |singular, plural| + define_method("test_pluralize_of_irregularity_#{plural}_should_be_the_same") do + with_dup do + ActiveSupport::Inflector.inflections do |inflect| + inflect.irregular(singular, plural) + assert_equal plural, ActiveSupport::Inflector.pluralize(plural) + end end end end - Irregularities.each do |irregularity| - singular, plural = *irregularity - ActiveSupport::Inflector.inflections do |inflect| - define_method("test_singularize_of_irregularity_#{singular}_should_be_the_same") do - inflect.irregular(singular, plural) - assert_equal singular, ActiveSupport::Inflector.singularize(singular) + Irregularities.each do |singular, plural| + define_method("test_singularize_of_irregularity_#{singular}_should_be_the_same") do + with_dup do + ActiveSupport::Inflector.inflections do |inflect| + inflect.irregular(singular, plural) + assert_equal singular, ActiveSupport::Inflector.singularize(singular) + end end end end diff --git a/activesupport/test/inflector_test_cases.rb b/activesupport/test/inflector_test_cases.rb index ca4efd2e59..cc36d62138 100644 --- a/activesupport/test/inflector_test_cases.rb +++ b/activesupport/test/inflector_test_cases.rb @@ -105,7 +105,6 @@ module InflectorTestCases "prize" => "prizes", "edge" => "edges", - "cow" => "kine", "database" => "databases", # regression tests against improper inflection regexes @@ -310,5 +309,6 @@ module InflectorTestCases 'move' => 'moves', 'cow' => 'kine', 'zombie' => 'zombies', + 'genus' => 'genera' } end diff --git a/activesupport/test/json/decoding_test.rb b/activesupport/test/json/decoding_test.rb index d1454902e5..e9780b36e4 100644 --- a/activesupport/test/json/decoding_test.rb +++ b/activesupport/test/json/decoding_test.rb @@ -4,6 +4,12 @@ require 'active_support/json' require 'active_support/time' class TestJSONDecoding < ActiveSupport::TestCase + class Foo + def self.json_create(object) + "Foo" + end + end + TESTS = { %q({"returnTo":{"\/categories":"\/"}}) => {"returnTo" => {"/categories" => "/"}}, %q({"return\\"To\\":":{"\/categories":"\/"}}) => {"return\"To\":" => {"/categories" => "/"}}, @@ -52,36 +58,50 @@ class TestJSONDecoding < ActiveSupport::TestCase # tests escaping of "\n" char with Yaml backend %q({"a":"\n"}) => {"a"=>"\n"}, %q({"a":"\u000a"}) => {"a"=>"\n"}, - %q({"a":"Line1\u000aLine2"}) => {"a"=>"Line1\nLine2"} + %q({"a":"Line1\u000aLine2"}) => {"a"=>"Line1\nLine2"}, + # prevent json unmarshalling + %q({"json_class":"TestJSONDecoding::Foo"}) => {"json_class"=>"TestJSONDecoding::Foo"}, + # json "fragments" - these are invalid JSON, but ActionPack relies on this + %q("a string") => "a string", + %q(1.1) => 1.1, + %q(1) => 1, + %q(-1) => -1, + %q(true) => true, + %q(false) => false, + %q(null) => nil } - backends = [:ok_json] - backends << :json_gem if defined?(::JSON) - backends << :yajl if defined?(::Yajl) - - backends.each do |backend| - TESTS.each do |json, expected| - test "json decodes #{json} with the #{backend} backend" do - ActiveSupport.parse_json_times = true - silence_warnings do - ActiveSupport::JSON.with_backend backend do - assert_equal expected, ActiveSupport::JSON.decode(json) - end - end + TESTS.each_with_index do |(json, expected), index| + test "json decodes #{index}" do + prev = ActiveSupport.parse_json_times + ActiveSupport.parse_json_times = true + silence_warnings do + assert_equal expected, ActiveSupport::JSON.decode(json), "JSON decoding \ + failed for #{json}" end + ActiveSupport.parse_json_times = prev end + end - test "json decodes time json with time parsing disabled with the #{backend} backend" do - ActiveSupport.parse_json_times = false - expected = {"a" => "2007-01-01 01:12:34 Z"} - ActiveSupport::JSON.with_backend backend do - assert_equal expected, ActiveSupport::JSON.decode(%({"a": "2007-01-01 01:12:34 Z"})) - end - end + test "json decodes time json with time parsing disabled" do + prev = ActiveSupport.parse_json_times + ActiveSupport.parse_json_times = false + expected = {"a" => "2007-01-01 01:12:34 Z"} + assert_equal expected, ActiveSupport::JSON.decode(%({"a": "2007-01-01 01:12:34 Z"})) + ActiveSupport.parse_json_times = prev end def test_failed_json_decoding + assert_raise(ActiveSupport::JSON.parse_error) { ActiveSupport::JSON.decode(%(undefined)) } + assert_raise(ActiveSupport::JSON.parse_error) { ActiveSupport::JSON.decode(%({a: 1})) } assert_raise(ActiveSupport::JSON.parse_error) { ActiveSupport::JSON.decode(%({: 1})) } + assert_raise(ActiveSupport::JSON.parse_error) { ActiveSupport::JSON.decode(%()) } + end + + def test_cannot_force_json_unmarshalling + encodeded = %q({"json_class":"TestJSONDecoding::Foo"}) + decodeded = {"json_class"=>"TestJSONDecoding::Foo"} + assert_equal decodeded, ActiveSupport::JSON.decode(encodeded, create_additions: true) end end diff --git a/activesupport/test/json/encoding_test.rb b/activesupport/test/json/encoding_test.rb index 5bb2a45c87..ed1326705c 100644 --- a/activesupport/test/json/encoding_test.rb +++ b/activesupport/test/json/encoding_test.rb @@ -45,8 +45,8 @@ class TestJSONEncoding < ActiveSupport::TestCase StringTests = [[ 'this is the <string>', %("this is the \\u003Cstring\\u003E")], [ 'a "string" with quotes & an ampersand', %("a \\"string\\" with quotes \\u0026 an ampersand") ], [ 'http://test.host/posts/1', %("http://test.host/posts/1")], - [ "Control characters: \x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f", - %("Control characters: \\u0000\\u0001\\u0002\\u0003\\u0004\\u0005\\u0006\\u0007\\b\\t\\n\\u000B\\f\\r\\u000E\\u000F\\u0010\\u0011\\u0012\\u0013\\u0014\\u0015\\u0016\\u0017\\u0018\\u0019\\u001A\\u001B\\u001C\\u001D\\u001E\\u001F") ]] + [ "Control characters: \x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\342\200\250\342\200\251", + %("Control characters: \\u0000\\u0001\\u0002\\u0003\\u0004\\u0005\\u0006\\u0007\\b\\t\\n\\u000B\\f\\r\\u000E\\u000F\\u0010\\u0011\\u0012\\u0013\\u0014\\u0015\\u0016\\u0017\\u0018\\u0019\\u001A\\u001B\\u001C\\u001D\\u001E\\u001F\\u2028\\u2029") ]] ArrayTests = [[ ['a', 'b', 'c'], %([\"a\",\"b\",\"c\"]) ], [ [1, 'a', :b, nil, false], %([1,\"a\",\"b\",null,false]) ]] @@ -82,6 +82,8 @@ class TestJSONEncoding < ActiveSupport::TestCase constants.grep(/Tests$/).each do |class_tests| define_method("test_#{class_tests[0..-6].underscore}") do begin + prev = ActiveSupport.use_standard_json_time_format + ActiveSupport.escape_html_entities_in_json = class_tests !~ /^Standard/ ActiveSupport.use_standard_json_time_format = class_tests =~ /^Standard/ self.class.const_get(class_tests).each do |pair| @@ -89,18 +91,11 @@ class TestJSONEncoding < ActiveSupport::TestCase end ensure ActiveSupport.escape_html_entities_in_json = false - ActiveSupport.use_standard_json_time_format = false + ActiveSupport.use_standard_json_time_format = prev end end end - def test_json_variable - assert_deprecated do - assert_equal ActiveSupport::JSON::Variable.new('foo'), 'foo' - assert_equal ActiveSupport::JSON::Variable.new('alert("foo")'), 'alert("foo")' - end - end - def test_hash_encoding assert_equal %({\"a\":\"b\"}), ActiveSupport::JSON.encode(:a => :b) assert_equal %({\"a\":1}), ActiveSupport::JSON.encode('a' => 1) @@ -112,21 +107,34 @@ class TestJSONEncoding < ActiveSupport::TestCase def test_utf8_string_encoded_properly result = ActiveSupport::JSON.encode('€2.99') - assert_equal '"\\u20ac2.99"', result + assert_equal '"€2.99"', result assert_equal(Encoding::UTF_8, result.encoding) result = ActiveSupport::JSON.encode('✎☺') - assert_equal '"\\u270e\\u263a"', result + assert_equal '"✎☺"', result assert_equal(Encoding::UTF_8, result.encoding) end def test_non_utf8_string_transcodes s = '二'.encode('Shift_JIS') result = ActiveSupport::JSON.encode(s) - assert_equal '"\\u4e8c"', result + assert_equal '"二"', result assert_equal Encoding::UTF_8, result.encoding end + def test_wide_utf8_chars + w = '𠜎' + result = ActiveSupport::JSON.encode(w) + assert_equal '"𠜎"', result + end + + def test_wide_utf8_roundtrip + hash = { string: "𐒑" } + json = ActiveSupport::JSON.encode(hash) + decoded_hash = ActiveSupport::JSON.decode(json) + assert_equal "𐒑", decoded_hash['string'] + end + def test_exception_raised_when_encoding_circular_reference_in_array a = [1] a << a @@ -159,23 +167,28 @@ class TestJSONEncoding < ActiveSupport::TestCase end def test_time_to_json_includes_local_offset + prev = ActiveSupport.use_standard_json_time_format ActiveSupport.use_standard_json_time_format = true with_env_tz 'US/Eastern' do assert_equal %("2005-02-01T15:15:10-05:00"), ActiveSupport::JSON.encode(Time.local(2005,2,1,15,15,10)) end ensure - ActiveSupport.use_standard_json_time_format = false + ActiveSupport.use_standard_json_time_format = prev end def test_hash_with_time_to_json + prev = ActiveSupport.use_standard_json_time_format + ActiveSupport.use_standard_json_time_format = false assert_equal '{"time":"2009/01/01 00:00:00 +0000"}', { :time => Time.utc(2009) }.to_json + ensure + ActiveSupport.use_standard_json_time_format = prev end def test_nested_hash_with_float assert_nothing_raised do hash = { "CHI" => { - :dislay_name => "chicago", + :display_name => "chicago", :latitude => 123.234 } } @@ -263,7 +276,8 @@ class TestJSONEncoding < ActiveSupport::TestCase f.bar = "world" hash = {"foo" => f, "other_hash" => {"foo" => "other_foo", "test" => "other_test"}} - assert_equal(%({"foo":{"foo":"hello","bar":"world"},"other_hash":{"foo":"other_foo","test":"other_test"}}), hash.to_json) + assert_equal({"foo"=>{"foo"=>"hello","bar"=>"world"}, + "other_hash" => {"foo"=>"other_foo","test"=>"other_test"}}, JSON.parse(hash.to_json)) end def test_struct_encoding @@ -288,12 +302,12 @@ class TestJSONEncoding < ActiveSupport::TestCase assert_equal({"name" => "David", "sub" => { "name" => "David", - "date" => "2010/01/01" }}, JSON.parse(json_custom)) + "date" => "2010-01-01" }}, JSON.parse(json_custom)) assert_equal({"name" => "David", "email" => "sample@example.com"}, JSON.parse(json_strings)) - assert_equal({"name" => "David", "date" => "2010/01/01"}, + assert_equal({"name" => "David", "date" => "2010-01-01"}, JSON.parse(json_string_and_date)) end diff --git a/activesupport/test/load_paths_test.rb b/activesupport/test/load_paths_test.rb index 979e25bdf3..ac617a9fd8 100644 --- a/activesupport/test/load_paths_test.rb +++ b/activesupport/test/load_paths_test.rb @@ -10,7 +10,7 @@ class LoadPathsTest < ActiveSupport::TestCase } load_paths_count[File.expand_path('../../lib', __FILE__)] -= 1 - filtered = load_paths_count.select { |k, v| v > 1 } - assert filtered.empty?, filtered.inspect + load_paths_count.select! { |k, v| v > 1 } + assert load_paths_count.empty?, load_paths_count.inspect end end diff --git a/activesupport/test/logger_test.rb b/activesupport/test/logger_test.rb index eedeca30a8..d2801849ca 100644 --- a/activesupport/test/logger_test.rb +++ b/activesupport/test/logger_test.rb @@ -120,4 +120,14 @@ 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 @output.string.include?("THIS IS HERE") + end end diff --git a/activesupport/test/message_encryptor_test.rb b/activesupport/test/message_encryptor_test.rb index d6c31396b6..509c453b5c 100644 --- a/activesupport/test/message_encryptor_test.rb +++ b/activesupport/test/message_encryptor_test.rb @@ -29,9 +29,9 @@ class MessageEncryptorTest < ActiveSupport::TestCase end def test_encrypting_twice_yields_differing_cipher_text - first_messqage = @encryptor.encrypt_and_sign(@data).split("--").first + first_message = @encryptor.encrypt_and_sign(@data).split("--").first second_message = @encryptor.encrypt_and_sign(@data).split("--").first - assert_not_equal first_messqage, second_message + assert_not_equal first_message, second_message end def test_messing_with_either_encrypted_values_causes_failure @@ -56,9 +56,14 @@ class MessageEncryptorTest < ActiveSupport::TestCase end def test_alternative_serialization_method + prev = ActiveSupport.use_standard_json_time_format + ActiveSupport.use_standard_json_time_format = true encryptor = ActiveSupport::MessageEncryptor.new(SecureRandom.hex(64), SecureRandom.hex(64), :serializer => JSONSerializer.new) message = encryptor.encrypt_and_sign({ :foo => 123, 'bar' => Time.utc(2010) }) - assert_equal encryptor.decrypt_and_verify(message), { "foo" => 123, "bar" => "2010-01-01T00:00:00Z" } + exp = { "foo" => 123, "bar" => "2010-01-01T00:00:00Z" } + assert_equal exp, encryptor.decrypt_and_verify(message) + ensure + ActiveSupport.use_standard_json_time_format = prev end private diff --git a/activesupport/test/message_verifier_test.rb b/activesupport/test/message_verifier_test.rb index 5adff41653..a8633f7299 100644 --- a/activesupport/test/message_verifier_test.rb +++ b/activesupport/test/message_verifier_test.rb @@ -45,9 +45,14 @@ class MessageVerifierTest < ActiveSupport::TestCase end def test_alternative_serialization_method + prev = ActiveSupport.use_standard_json_time_format + ActiveSupport.use_standard_json_time_format = true verifier = ActiveSupport::MessageVerifier.new("Hey, I'm a secret!", :serializer => JSONSerializer.new) message = verifier.generate({ :foo => 123, 'bar' => Time.utc(2010) }) - assert_equal verifier.verify(message), { "foo" => 123, "bar" => "2010-01-01T00:00:00Z" } + exp = { "foo" => 123, "bar" => "2010-01-01T00:00:00Z" } + assert_equal exp, verifier.verify(message) + ensure + ActiveSupport.use_standard_json_time_format = prev end def assert_not_verified(message) diff --git a/activesupport/test/notifications/instrumenter_test.rb b/activesupport/test/notifications/instrumenter_test.rb new file mode 100644 index 0000000000..f46e96f636 --- /dev/null +++ b/activesupport/test/notifications/instrumenter_test.rb @@ -0,0 +1,58 @@ +require 'abstract_unit' +require 'active_support/notifications/instrumenter' + +module ActiveSupport + module Notifications + class InstrumenterTest < ActiveSupport::TestCase + class TestNotifier + attr_reader :starts, :finishes + + def initialize + @starts = [] + @finishes = [] + end + + def start(*args); @starts << args; end + def finish(*args); @finishes << args; end + end + + attr_reader :instrumenter, :notifier, :payload + + def setup + super + @notifier = TestNotifier.new + @instrumenter = Instrumenter.new @notifier + @payload = { :foo => Object.new } + end + + def test_instrument + called = false + instrumenter.instrument("foo", payload) { + called = true + } + + assert called + end + + def test_instrument_yields_the_payload_for_further_modification + assert_equal 2, instrumenter.instrument("awesome") { |p| p[:result] = 1 + 1 } + assert_equal 1, notifier.finishes.size + name, _, payload = notifier.finishes.first + assert_equal "awesome", name + assert_equal Hash[:result => 2], payload + end + + def test_start + instrumenter.start("foo", payload) + assert_equal [["foo", instrumenter.id, payload]], notifier.starts + assert_predicate notifier.finishes, :empty? + end + + def test_finish + instrumenter.finish("foo", payload) + assert_equal [["foo", instrumenter.id, payload]], notifier.finishes + assert_predicate notifier.starts, :empty? + end + end + end +end diff --git a/activesupport/test/notifications_test.rb b/activesupport/test/notifications_test.rb index bcb393c7bc..f729f0a95b 100644 --- a/activesupport/test/notifications_test.rb +++ b/activesupport/test/notifications_test.rb @@ -81,6 +81,20 @@ module Notifications end end + class TestSubscriber + attr_reader :starts, :finishes, :publishes + + def initialize + @starts = [] + @finishes = [] + @publishes = [] + end + + def start(*args); @starts << args; end + def finish(*args); @finishes << args; end + def publish(*args); @publishes << args; end + end + class SyncPubSubTest < TestCase def test_events_are_published_to_a_listener @notifier.publish :foo @@ -99,7 +113,7 @@ module Notifications @notifier.publish :foo @notifier.publish :foo - @notifier.subscribe("not_existant") do |*args| + @notifier.subscribe("not_existent") do |*args| @events << ActiveSupport::Notifications::Event.new(*args) end @@ -144,6 +158,14 @@ module Notifications assert_equal [[:foo]], @another end + def test_publish_with_subscriber + subscriber = TestSubscriber.new + @notifier.subscribe nil, subscriber + @notifier.publish :foo + + assert_equal [[:foo]], subscriber.publishes + end + private def event(*args) args @@ -157,7 +179,7 @@ module Notifications assert_equal 2, instrument(:awesome) { 1 + 1 } end - def test_instrument_yields_the_paylod_for_further_modification + def test_instrument_yields_the_payload_for_further_modification assert_equal 2, instrument(:awesome) { |p| p[:result] = 1 + 1 } assert_equal 1, @events.size assert_equal :awesome, @events.first.name diff --git a/activesupport/test/number_helper_test.rb b/activesupport/test/number_helper_test.rb index 5f54587f93..1fadef3637 100644 --- a/activesupport/test/number_helper_test.rb +++ b/activesupport/test/number_helper_test.rb @@ -301,6 +301,13 @@ module ActiveSupport end end + def test_number_to_human_with_custom_units_that_are_missing_the_needed_key + [@instance_with_helpers, TestClassWithClassNumberHelpers, ActiveSupport::NumberHelper].each do |number_helper| + assert_equal '123', number_helper.number_to_human(123, units: { thousand: 'k'}) + assert_equal '123', number_helper.number_to_human(123, units: {}) + end + end + def test_number_to_human_with_custom_format [@instance_with_helpers, TestClassWithClassNumberHelpers, ActiveSupport::NumberHelper].each do |number_helper| assert_equal '123 times Thousand', number_helper.number_to_human(123456, :format => "%n times %u") diff --git a/activesupport/test/ordered_hash_test.rb b/activesupport/test/ordered_hash_test.rb index 14ba4e0076..c3fe89de4b 100644 --- a/activesupport/test/ordered_hash_test.rb +++ b/activesupport/test/ordered_hash_test.rb @@ -2,6 +2,7 @@ require 'abstract_unit' require 'active_support/json' require 'active_support/core_ext/object/to_json' require 'active_support/core_ext/hash/indifferent_access' +require 'active_support/core_ext/array/extract_options' class OrderedHashTest < ActiveSupport::TestCase def setup @@ -243,11 +244,7 @@ class OrderedHashTest < ActiveSupport::TestCase end def test_each_after_yaml_serialization - values = [] - @deserialized_ordered_hash = YAML.load(YAML.dump(@ordered_hash)) - - @deserialized_ordered_hash.each {|key, value| values << value} - assert_equal @values, values + assert_equal @values, YAML.load(YAML.dump(@ordered_hash)).values end def test_each_when_yielding_to_block_with_splat diff --git a/activesupport/test/ordered_options_test.rb b/activesupport/test/ordered_options_test.rb index f60f9a58e3..fdc745b23b 100644 --- a/activesupport/test/ordered_options_test.rb +++ b/activesupport/test/ordered_options_test.rb @@ -7,13 +7,13 @@ class OrderedOptionsTest < ActiveSupport::TestCase assert_nil a[:not_set] - a[:allow_concurreny] = true + a[:allow_concurrency] = true assert_equal 1, a.size - assert a[:allow_concurreny] + assert a[:allow_concurrency] - a[:allow_concurreny] = false + a[:allow_concurrency] = false assert_equal 1, a.size - assert !a[:allow_concurreny] + assert !a[:allow_concurrency] a["else_where"] = 56 assert_equal 2, a.size @@ -23,10 +23,10 @@ class OrderedOptionsTest < ActiveSupport::TestCase def test_looping a = ActiveSupport::OrderedOptions.new - a[:allow_concurreny] = true + a[:allow_concurrency] = true a["else_where"] = 56 - test = [[:allow_concurreny, true], [:else_where, 56]] + test = [[:allow_concurrency, true], [:else_where, 56]] a.each_with_index do |(key, value), index| assert_equal test[index].first, key @@ -39,13 +39,13 @@ class OrderedOptionsTest < ActiveSupport::TestCase assert_nil a.not_set - a.allow_concurreny = true + a.allow_concurrency = true assert_equal 1, a.size - assert a.allow_concurreny + assert a.allow_concurrency - a.allow_concurreny = false + a.allow_concurrency = false assert_equal 1, a.size - assert !a.allow_concurreny + assert !a.allow_concurrency a.else_where = 56 assert_equal 2, a.size diff --git a/activesupport/test/queueing/synchronous_queue_test.rb b/activesupport/test/queueing/synchronous_queue_test.rb deleted file mode 100644 index 86c39d0f6c..0000000000 --- a/activesupport/test/queueing/synchronous_queue_test.rb +++ /dev/null @@ -1,27 +0,0 @@ -require 'abstract_unit' -require 'active_support/queueing' - -class SynchronousQueueTest < ActiveSupport::TestCase - class Job - attr_reader :ran - def run; @ran = true end - end - - class ExceptionRaisingJob - def run; raise end - end - - def setup - @queue = ActiveSupport::SynchronousQueue.new - end - - def test_runs_jobs_immediately - job = Job.new - @queue.push job - assert job.ran - - assert_raises RuntimeError do - @queue.push ExceptionRaisingJob.new - end - end -end diff --git a/activesupport/test/queueing/test_queue_test.rb b/activesupport/test/queueing/test_queue_test.rb deleted file mode 100644 index 451fb68d3e..0000000000 --- a/activesupport/test/queueing/test_queue_test.rb +++ /dev/null @@ -1,146 +0,0 @@ -require 'abstract_unit' -require 'active_support/queueing' - -class TestQueueTest < ActiveSupport::TestCase - def setup - @queue = ActiveSupport::TestQueue.new - end - - class ExceptionRaisingJob - def run - raise - end - end - - def test_drain_raises_exceptions_from_running_jobs - @queue.push ExceptionRaisingJob.new - assert_raises(RuntimeError) { @queue.drain } - end - - def test_jobs - @queue.push 1 - @queue.push 2 - assert_equal [1,2], @queue.jobs - end - - class EquivalentJob - def initialize - @initial_id = self.object_id - end - - def run - end - - def ==(other) - other.same_initial_id?(@initial_id) - end - - def same_initial_id?(other_id) - other_id == @initial_id - end - end - - def test_contents - job = EquivalentJob.new - assert @queue.empty? - @queue.push job - refute @queue.empty? - assert_equal job, @queue.pop - end - - class ProcessingJob - def self.clear_processed - @processed = [] - end - - def self.processed - @processed - end - - def initialize(object) - @object = object - end - - def run - self.class.processed << @object - end - end - - def test_order - ProcessingJob.clear_processed - job1 = ProcessingJob.new(1) - job2 = ProcessingJob.new(2) - - @queue.push job1 - @queue.push job2 - @queue.drain - - assert_equal [1,2], ProcessingJob.processed - end - - class ThreadTrackingJob - attr_reader :thread_id - - def run - @thread_id = Thread.current.object_id - end - - def ran? - @thread_id - end - end - - def test_drain - @queue.push ThreadTrackingJob.new - job = @queue.jobs.last - @queue.drain - - assert @queue.empty? - assert job.ran?, "The job runs synchronously when the queue is drained" - assert_equal job.thread_id, Thread.current.object_id - end - - class IdentifiableJob - def initialize(id) - @id = id - end - - def ==(other) - other.same_id?(@id) - end - - def same_id?(other_id) - other_id == @id - end - - def run - end - end - - def test_queue_can_be_observed - jobs = (1..10).map do |id| - IdentifiableJob.new(id) - end - - jobs.each do |job| - @queue.push job - end - - assert_equal jobs, @queue.jobs - end - - def test_adding_an_unmarshallable_job - anonymous_class_instance = Struct.new(:run).new - - assert_raises TypeError do - @queue.push anonymous_class_instance - end - end - - def test_attempting_to_add_a_reference_to_itself - job = {reference: @queue} - assert_raises TypeError do - @queue.push job - end - end -end diff --git a/activesupport/test/queueing/threaded_consumer_test.rb b/activesupport/test/queueing/threaded_consumer_test.rb deleted file mode 100644 index a3ca46a261..0000000000 --- a/activesupport/test/queueing/threaded_consumer_test.rb +++ /dev/null @@ -1,110 +0,0 @@ -require 'abstract_unit' -require 'active_support/queueing' -require "active_support/log_subscriber/test_helper" - -class TestThreadConsumer < ActiveSupport::TestCase - class Job - attr_reader :id - def initialize(id = 1, &block) - @id = id - @block = block - end - - def run - @block.call if @block - end - end - - def setup - @logger = ActiveSupport::LogSubscriber::TestHelper::MockLogger.new - @queue = ActiveSupport::Queue.new(logger: @logger) - end - - def teardown - @queue.drain - end - - test "the jobs are executed" do - ran = false - job = Job.new { ran = true } - - @queue.push job - @queue.drain - - assert_equal true, ran - end - - test "the jobs are not executed synchronously" do - run, ran = Queue.new, Queue.new - job = Job.new { ran.push run.pop } - - @queue.consumer.start - @queue.push job - assert ran.empty? - - run.push true - assert_equal true, ran.pop - end - - test "shutting down the queue synchronously drains the jobs" do - ran = false - job = Job.new do - sleep 0.1 - ran = true - end - - @queue.consumer.start - @queue.push job - assert_equal false, ran - - @queue.consumer.shutdown - assert_equal true, ran - end - - test "log job that raises an exception" do - job = Job.new { raise "RuntimeError: Error!" } - - @queue.push job - consume_queue @queue - - assert_equal 1, @logger.logged(:error).size - assert_match "Job Error: #{job.inspect}\nRuntimeError: Error!", @logger.logged(:error).last - end - - test "logger defaults to stderr" do - begin - $stderr, old_stderr = StringIO.new, $stderr - queue = ActiveSupport::Queue.new - queue.push Job.new { raise "RuntimeError: Error!" } - consume_queue queue - assert_match 'Job Error', $stderr.string - ensure - $stderr = old_stderr - end - end - - test "test overriding exception handling" do - @queue.consumer.instance_eval do - def handle_exception(job, exception) - @last_error = exception.message - end - - def last_error - @last_error - end - end - - job = Job.new { raise "RuntimeError: Error!" } - - @queue.push job - consume_queue @queue - - assert_equal "RuntimeError: Error!", @queue.consumer.last_error - end - - private - def consume_queue(queue) - queue.push nil - queue.consumer.consume - end -end diff --git a/activesupport/test/rescuable_test.rb b/activesupport/test/rescuable_test.rb index 3f8d09c18e..ec9d231125 100644 --- a/activesupport/test/rescuable_test.rb +++ b/activesupport/test/rescuable_test.rb @@ -21,7 +21,7 @@ class Stargate rescue_from WraithAttack, :with => :sos - rescue_from NuclearExplosion do + rescue_from 'NuclearExplosion' do @result = 'alldead' end @@ -97,10 +97,9 @@ class RescuableTest < ActiveSupport::TestCase assert_equal expected, result end - def test_children_should_inherit_rescue_defintions_from_parents_and_child_rescue_should_be_appended + def test_children_should_inherit_rescue_definitions_from_parents_and_child_rescue_should_be_appended expected = ["WraithAttack", "WraithAttack", "NuclearExplosion", "MadRonon", "CoolError"] result = @cool_stargate.send(:rescue_handlers).collect {|e| e.first} assert_equal expected, result end - end diff --git a/activesupport/test/spec_type_test.rb b/activesupport/test/spec_type_test.rb deleted file mode 100644 index 9a6cb4ded2..0000000000 --- a/activesupport/test/spec_type_test.rb +++ /dev/null @@ -1,22 +0,0 @@ -require "abstract_unit" -require "active_record" - -class SomeRandomModel < ActiveRecord::Base; end - -class SpecTypeTest < ActiveSupport::TestCase - def assert_support actual - assert_equal ActiveSupport::TestCase, actual - end - - def assert_spec actual - assert_equal MiniTest::Spec, actual - end - - def test_spec_type_resolves_for_active_record_constants - assert_support MiniTest::Spec.spec_type(SomeRandomModel) - end - - def test_spec_type_doesnt_resolve_random_strings - assert_spec MiniTest::Spec.spec_type("Unmatched String") - end -end diff --git a/activesupport/test/string_inquirer_test.rb b/activesupport/test/string_inquirer_test.rb index 94d5fe197d..a2ed577eb0 100644 --- a/activesupport/test/string_inquirer_test.rb +++ b/activesupport/test/string_inquirer_test.rb @@ -10,7 +10,7 @@ class StringInquirerTest < ActiveSupport::TestCase end def test_miss - refute @string_inquirer.development? + assert_not @string_inquirer.development? end def test_missing_question_mark diff --git a/activesupport/test/subscriber_test.rb b/activesupport/test/subscriber_test.rb new file mode 100644 index 0000000000..253411aa3d --- /dev/null +++ b/activesupport/test/subscriber_test.rb @@ -0,0 +1,40 @@ +require 'abstract_unit' +require 'active_support/subscriber' + +class TestSubscriber < ActiveSupport::Subscriber + attach_to :doodle + + cattr_reader :event + + def self.clear + @@event = nil + end + + def open_party(event) + @@event = event + end + + private + + def private_party(event) + @@event = event + end +end + +class SubscriberTest < ActiveSupport::TestCase + def setup + TestSubscriber.clear + end + + def test_attaches_subscribers + ActiveSupport::Notifications.instrument("open_party.doodle") + + assert_equal "open_party.doodle", TestSubscriber.event.name + end + + def test_does_not_attach_private_methods + ActiveSupport::Notifications.instrument("private_party.doodle") + + assert_nil TestSubscriber.event + end +end diff --git a/activesupport/test/test_case_test.rb b/activesupport/test/test_case_test.rb deleted file mode 100644 index dfe9f3c11c..0000000000 --- a/activesupport/test/test_case_test.rb +++ /dev/null @@ -1,118 +0,0 @@ -require 'abstract_unit' - -module ActiveSupport - class TestCaseTest < ActiveSupport::TestCase - class FakeRunner - attr_reader :puked - - def initialize - @puked = [] - end - - def puke(klass, name, e) - @puked << [klass, name, e] - end - - def options - nil - end - - def record(*args) - end - end - - def test_standard_error_raised_within_setup_callback_is_puked - tc = Class.new(TestCase) do - def self.name; nil; end - - setup :bad_callback - def bad_callback; raise 'oh noes' end - def test_true; assert true end - end - - test_name = 'test_true' - fr = FakeRunner.new - - test = tc.new test_name - test.run fr - klass, name, exception = *fr.puked.first - - assert_equal tc, klass - assert_equal test_name, name - assert_equal 'oh noes', exception.message - end - - def test_standard_error_raised_within_teardown_callback_is_puked - tc = Class.new(TestCase) do - def self.name; nil; end - - teardown :bad_callback - def bad_callback; raise 'oh noes' end - def test_true; assert true end - end - - test_name = 'test_true' - fr = FakeRunner.new - - test = tc.new test_name - test.run fr - klass, name, exception = *fr.puked.first - - assert_equal tc, klass - assert_equal test_name, name - assert_equal 'oh noes', exception.message - end - - def test_passthrough_exception_raised_within_test_method_is_not_rescued - tc = Class.new(TestCase) do - def self.name; nil; end - - def test_which_raises_interrupt; raise Interrupt; end - end - - test_name = 'test_which_raises_interrupt' - fr = FakeRunner.new - - test = tc.new test_name - assert_raises(Interrupt) { test.run fr } - end - - def test_passthrough_exception_raised_within_setup_callback_is_not_rescued - tc = Class.new(TestCase) do - def self.name; nil; end - - setup :callback_which_raises_interrupt - def callback_which_raises_interrupt; raise Interrupt; end - def test_true; assert true end - end - - test_name = 'test_true' - fr = FakeRunner.new - - test = tc.new test_name - assert_raises(Interrupt) { test.run fr } - end - - def test_passthrough_exception_raised_within_teardown_callback_is_not_rescued - tc = Class.new(TestCase) do - def self.name; nil; end - - teardown :callback_which_raises_interrupt - def callback_which_raises_interrupt; raise Interrupt; end - def test_true; assert true end - end - - test_name = 'test_true' - fr = FakeRunner.new - - test = tc.new test_name - assert_raises(Interrupt) { test.run fr } - end - - def test_pending_deprecation - assert_deprecated do - pending "should use #skip instead" - end - end - end -end diff --git a/activesupport/test/test_test.rb b/activesupport/test/test_test.rb index 9516556844..0c8cc1f883 100644 --- a/activesupport/test/test_test.rb +++ b/activesupport/test/test_test.rb @@ -15,6 +15,17 @@ class AssertDifferenceTest < ActiveSupport::TestCase @object.num = 0 end + def test_assert_not + assert_equal true, assert_not(nil) + assert_equal true, assert_not(false) + + e = assert_raises(MiniTest::Assertion) { assert_not true } + assert_equal 'Expected true to be nil or false', e.message + + e = assert_raises(MiniTest::Assertion) { assert_not true, 'custom' } + assert_equal 'custom', e.message + end + def test_assert_no_difference assert_no_difference '@object.num' do # ... @@ -76,58 +87,18 @@ class AssertDifferenceTest < ActiveSupport::TestCase end end -class AssertBlankTest < ActiveSupport::TestCase - BLANK = [ EmptyTrue.new, nil, false, '', ' ', " \n\t \r ", [], {} ] - NOT_BLANK = [ EmptyFalse.new, Object.new, true, 0, 1, 'x', [nil], { nil => 0 } ] - - def test_assert_blank_true - BLANK.each { |v| assert_blank v } - end - - def test_assert_blank_false - NOT_BLANK.each { |v| - begin - assert_blank v - fail 'should not get to here' - rescue Exception => e - assert_match(/is not blank/, e.message) - end - } - end -end - -class AssertPresentTest < ActiveSupport::TestCase - BLANK = [ EmptyTrue.new, nil, false, '', ' ', " \n\t \r ", [], {} ] - NOT_BLANK = [ EmptyFalse.new, Object.new, true, 0, 1, 'x', [nil], { nil => 0 } ] - - def test_assert_present_true - NOT_BLANK.each { |v| assert_present v } - end - - def test_assert_present_false - BLANK.each { |v| - begin - assert_present v - fail 'should not get to here' - rescue Exception => e - assert_match(/is blank/, e.message) - end - } - end -end - class AlsoDoingNothingTest < ActiveSupport::TestCase end # Setup and teardown callbacks. class SetupAndTeardownTest < ActiveSupport::TestCase setup :reset_callback_record, :foo - teardown :foo, :sentinel, :foo + teardown :foo, :sentinel def test_inherited_setup_callbacks assert_equal [:reset_callback_record, :foo], self.class._setup_callbacks.map(&:raw_filter) assert_equal [:foo], @called_back - assert_equal [:foo, :sentinel, :foo], self.class._teardown_callbacks.map(&:raw_filter) + assert_equal [:foo, :sentinel], self.class._teardown_callbacks.map(&:raw_filter) end def setup @@ -147,7 +118,7 @@ class SetupAndTeardownTest < ActiveSupport::TestCase end def sentinel - assert_equal [:foo, :foo], @called_back + assert_equal [:foo], @called_back end end @@ -159,7 +130,7 @@ class SubclassSetupAndTeardownTest < SetupAndTeardownTest def test_inherited_setup_callbacks assert_equal [:reset_callback_record, :foo, :bar], self.class._setup_callbacks.map(&:raw_filter) assert_equal [:foo, :bar], @called_back - assert_equal [:foo, :sentinel, :foo, :bar], self.class._teardown_callbacks.map(&:raw_filter) + assert_equal [:foo, :sentinel, :bar], self.class._teardown_callbacks.map(&:raw_filter) end protected @@ -168,7 +139,7 @@ class SubclassSetupAndTeardownTest < SetupAndTeardownTest end def sentinel - assert_equal [:foo, :bar, :bar, :foo], @called_back + assert_equal [:foo, :bar, :bar], @called_back end end @@ -182,7 +153,6 @@ class TestCaseTaggedLoggingTest < ActiveSupport::TestCase end def test_logs_tagged_with_current_test_case - tagged_logger.info 'test' - assert_equal "[#{self.class.name}] [#{__name__}] test\n", @out.string + assert_match "#{self.class}: #{name}\n", @out.string end end diff --git a/activesupport/test/testing/constant_lookup_test.rb b/activesupport/test/testing/constant_lookup_test.rb index 19280ba74a..aca2951450 100644 --- a/activesupport/test/testing/constant_lookup_test.rb +++ b/activesupport/test/testing/constant_lookup_test.rb @@ -1,4 +1,5 @@ require 'abstract_unit' +require 'dependencies_test_helpers' class Foo; end class Bar < Foo @@ -10,6 +11,7 @@ module FooBar; end class ConstantLookupTest < ActiveSupport::TestCase include ActiveSupport::Testing::ConstantLookup + include DependenciesTestHelpers def find_foo(name) self.class.determine_constant_from_test_name(name) do |constant| @@ -56,4 +58,12 @@ class ConstantLookupTest < ActiveSupport::TestCase assert_nil find_module("DoesntExist::Nadda::Nope") assert_nil find_module("DoesntExist::Nadda::Nope::NotHere") end + + def test_does_not_swallow_exception_on_no_method_error + assert_raises(NoMethodError) { + with_autoloading_fixtures { + self.class.determine_constant_from_test_name("RaisesNoMethodError") + } + } + end end diff --git a/activesupport/test/testing/performance_test.rb b/activesupport/test/testing/performance_test.rb deleted file mode 100644 index 6918110cce..0000000000 --- a/activesupport/test/testing/performance_test.rb +++ /dev/null @@ -1,68 +0,0 @@ -require 'abstract_unit' - -module ActiveSupport - module Testing - class PerformanceTest < ActiveSupport::TestCase - begin - require 'active_support/testing/performance' - HAVE_RUBYPROF = true - rescue LoadError - HAVE_RUBYPROF = false - end - - def setup - skip "no rubyprof" unless HAVE_RUBYPROF - end - - def test_amount_format - amount_metric = ActiveSupport::Testing::Performance::Metrics[:amount].new - assert_equal "0", amount_metric.format(0) - assert_equal "1", amount_metric.format(1.23) - assert_equal "40,000,000", amount_metric.format(40000000) - end - - def test_time_format - time_metric = ActiveSupport::Testing::Performance::Metrics[:time].new - assert_equal "0 ms", time_metric.format(0) - assert_equal "40 ms", time_metric.format(0.04) - assert_equal "41 ms", time_metric.format(0.0415) - assert_equal "1.23 sec", time_metric.format(1.23) - assert_equal "40000.00 sec", time_metric.format(40000) - assert_equal "-5000 ms", time_metric.format(-5) - end - - def test_space_format - space_metric = ActiveSupport::Testing::Performance::Metrics[:digital_information_unit].new - assert_equal "0 Bytes", space_metric.format(0) - assert_equal "0 Bytes", space_metric.format(0.4) - assert_equal "1 Byte", space_metric.format(1.23) - assert_equal "123 Bytes", space_metric.format(123) - assert_equal "123 Bytes", space_metric.format(123.45) - assert_equal "12 KB", space_metric.format(12345) - assert_equal "1.2 MB", space_metric.format(1234567) - assert_equal "9.3 GB", space_metric.format(10**10) - assert_equal "91 TB", space_metric.format(10**14) - assert_equal "910000 TB", space_metric.format(10**18) - end - - def test_environment_format_without_rails - metric = ActiveSupport::Testing::Performance::Metrics[:time].new - benchmarker = ActiveSupport::Testing::Performance::Benchmarker.new(self, metric) - assert_equal "#{RUBY_ENGINE}-#{RUBY_VERSION}.#{RUBY_PATCHLEVEL},#{RUBY_PLATFORM}", benchmarker.environment - end - - def test_environment_format_with_rails - rails, version = Module.new, Module.new - version.const_set :STRING, "4.0.0" - rails.const_set :VERSION, version - Object.const_set :Rails, rails - - metric = ActiveSupport::Testing::Performance::Metrics[:time].new - benchmarker = ActiveSupport::Testing::Performance::Benchmarker.new(self, metric) - assert_equal "rails-4.0.0,#{RUBY_ENGINE}-#{RUBY_VERSION}.#{RUBY_PATCHLEVEL},#{RUBY_PLATFORM}", benchmarker.environment - ensure - Object.send :remove_const, :Rails - end - end - end -end diff --git a/activesupport/test/time_zone_test.rb b/activesupport/test/time_zone_test.rb index 9c3b5d0667..84c3154e53 100644 --- a/activesupport/test/time_zone_test.rb +++ b/activesupport/test/time_zone_test.rb @@ -232,6 +232,22 @@ class TimeZoneTest < ActiveSupport::TestCase assert_equal Time.utc(2012, 5, 28, 7, 0, 0), twz.utc end + def test_parse_doesnt_use_local_dst + with_env_tz 'US/Eastern' do + zone = ActiveSupport::TimeZone['UTC'] + twz = zone.parse('2013-03-10 02:00:00') + assert_equal Time.utc(2013, 3, 10, 2, 0, 0), twz.time + end + end + + def test_parse_handles_dst_jump + with_env_tz 'US/Eastern' do + zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)'] + twz = zone.parse('2013-03-10 02:00:00') + assert_equal Time.utc(2013, 3, 10, 3, 0, 0), twz.time + end + end + def test_utc_offset_lazy_loaded_from_tzinfo_when_not_passed_in_to_initialize tzinfo = TZInfo::Timezone.get('America/New_York') zone = ActiveSupport::TimeZone.create(tzinfo.name, nil, tzinfo) diff --git a/activesupport/test/transliterate_test.rb b/activesupport/test/transliterate_test.rb index b5d8142458..e0f85f4e7c 100644 --- a/activesupport/test/transliterate_test.rb +++ b/activesupport/test/transliterate_test.rb @@ -3,7 +3,6 @@ require 'abstract_unit' require 'active_support/inflector/transliterate' class TransliterateTest < ActiveSupport::TestCase - def test_transliterate_should_not_change_ascii_chars (0..127).each do |byte| char = [byte].pack("U") @@ -17,19 +16,20 @@ class TransliterateTest < ActiveSupport::TestCase # some reason or other are floating in the middle of all the letters. string = (0xC0..0x17E).to_a.reject {|c| [0xD7, 0xF7].include?(c)}.pack("U*") string.each_char do |char| - assert_match %r{^[a-zA-Z']*$}, ActiveSupport::Inflector.transliterate(string) + assert_match %r{^[a-zA-Z']*$}, ActiveSupport::Inflector.transliterate(char) end end def test_transliterate_should_work_with_custom_i18n_rules_and_uncomposed_utf8 char = [117, 776].pack("U*") # "ü" as ASCII "u" plus COMBINING DIAERESIS I18n.backend.store_translations(:de, :i18n => {:transliterate => {:rule => {"ü" => "ue"}}}) - I18n.locale = :de + default_locale, I18n.locale = I18n.locale, :de assert_equal "ue", ActiveSupport::Inflector.transliterate(char) + ensure + I18n.locale = default_locale end def test_transliterate_should_allow_a_custom_replacement_char assert_equal "a*b", ActiveSupport::Inflector.transliterate("a索b", "*") end - end diff --git a/activesupport/test/ts_isolated.rb b/activesupport/test/ts_isolated.rb deleted file mode 100644 index 2c217157d3..0000000000 --- a/activesupport/test/ts_isolated.rb +++ /dev/null @@ -1,16 +0,0 @@ -require 'minitest/autorun' -require 'active_support/test_case' -require 'rbconfig' -require 'active_support/core_ext/kernel/reporting' - -class TestIsolated < ActiveSupport::TestCase - ruby = File.join(*RbConfig::CONFIG.values_at('bindir', 'RUBY_INSTALL_NAME')) - - Dir["#{File.dirname(__FILE__)}/**/*_test.rb"].each do |file| - define_method("test #{file}") do - command = "#{ruby} -Ilib:test #{file}" - result = silence_stderr { `#{command}` } - assert $?.to_i.zero?, "#{command}\n#{result}" - end - end -end diff --git a/activesupport/test/xml_mini/jdom_engine_test.rb b/activesupport/test/xml_mini/jdom_engine_test.rb index f77d78d42c..ed4de8aba2 100644 --- a/activesupport/test/xml_mini/jdom_engine_test.rb +++ b/activesupport/test/xml_mini/jdom_engine_test.rb @@ -3,9 +3,12 @@ if RUBY_PLATFORM =~ /java/ require 'active_support/xml_mini' require 'active_support/core_ext/hash/conversions' + class JDOMEngineTest < ActiveSupport::TestCase include ActiveSupport + FILES_DIR = File.dirname(__FILE__) + '/../fixtures/xml' + def setup @default_backend = XmlMini.backend XmlMini.backend = 'JDOM' @@ -30,10 +33,41 @@ if RUBY_PLATFORM =~ /java/ assert_equal 'image/png', file.content_type end + def test_not_allowed_to_expand_entities_to_files + attack_xml = <<-EOT + <!DOCTYPE member [ + <!ENTITY a SYSTEM "file://#{FILES_DIR}/jdom_include.txt"> + ]> + <member>x&a;</member> + EOT + assert_equal 'x', Hash.from_xml(attack_xml)["member"] + end + + def test_not_allowed_to_expand_parameter_entities_to_files + attack_xml = <<-EOT + <!DOCTYPE member [ + <!ENTITY % b SYSTEM "file://#{FILES_DIR}/jdom_entities.txt"> + %b; + ]> + <member>x&a;</member> + EOT + assert_raise Java::OrgXmlSax::SAXParseException do + assert_equal 'x', Hash.from_xml(attack_xml)["member"] + end + end + + + def test_not_allowed_to_load_external_doctypes + attack_xml = <<-EOT + <!DOCTYPE member SYSTEM "file://#{FILES_DIR}/jdom_doctype.dtd"> + <member>x&a;</member> + EOT + assert_equal 'x', Hash.from_xml(attack_xml)["member"] + end + def test_exception_thrown_on_expansion_attack - assert_raise NativeException do + assert_raise Java::OrgXmlSax::SAXParseException do attack_xml = <<-EOT - <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE member [ <!ENTITY a "&b;&b;&b;&b;&b;&b;&b;&b;&b;&b;"> <!ENTITY b "&c;&c;&c;&c;&c;&c;&c;&c;&c;&c;"> @@ -142,10 +176,11 @@ if RUBY_PLATFORM =~ /java/ end private - def assert_equal_rexml(xml) - hash = XmlMini.with_backend('REXML') { XmlMini.parse(xml) } - assert_equal(hash, XmlMini.parse(xml)) - end + def assert_equal_rexml(xml) + parsed_xml = XmlMini.parse(xml) + hash = XmlMini.with_backend('REXML') { XmlMini.parse(xml) } + assert_equal(hash, parsed_xml) + end end else diff --git a/activesupport/test/xml_mini/libxml_engine_test.rb b/activesupport/test/xml_mini/libxml_engine_test.rb index 5debb2fd59..a8df2e1f7b 100644 --- a/activesupport/test/xml_mini/libxml_engine_test.rb +++ b/activesupport/test/xml_mini/libxml_engine_test.rb @@ -1,12 +1,11 @@ -require 'abstract_unit' -require 'active_support/xml_mini' -require 'active_support/core_ext/hash/conversions' - begin require 'libxml' rescue LoadError # Skip libxml tests else +require 'abstract_unit' +require 'active_support/xml_mini' +require 'active_support/core_ext/hash/conversions' class LibxmlEngineTest < ActiveSupport::TestCase include ActiveSupport @@ -142,7 +141,7 @@ class LibxmlEngineTest < ActiveSupport::TestCase morning </root> eoxml - XmlMini.parse(io) + assert_equal_rexml(io) end def test_children_with_simple_cdata @@ -194,10 +193,12 @@ class LibxmlEngineTest < ActiveSupport::TestCase private - def assert_equal_rexml(xml) - hash = XmlMini.with_backend('REXML') { XmlMini.parse(xml) } - assert_equal(hash, XmlMini.parse(xml)) - end + def assert_equal_rexml(xml) + parsed_xml = XmlMini.parse(xml) + xml.rewind if xml.respond_to?(:rewind) + hash = XmlMini.with_backend('REXML') { XmlMini.parse(xml) } + assert_equal(hash, parsed_xml) + end end end diff --git a/activesupport/test/xml_mini/libxmlsax_engine_test.rb b/activesupport/test/xml_mini/libxmlsax_engine_test.rb index 94250d48ec..d6d90639e2 100644 --- a/activesupport/test/xml_mini/libxmlsax_engine_test.rb +++ b/activesupport/test/xml_mini/libxmlsax_engine_test.rb @@ -1,12 +1,11 @@ -require 'abstract_unit' -require 'active_support/xml_mini' -require 'active_support/core_ext/hash/conversions' - begin require 'libxml' rescue LoadError # Skip libxml tests else +require 'abstract_unit' +require 'active_support/xml_mini' +require 'active_support/core_ext/hash/conversions' class LibXMLSAXEngineTest < ActiveSupport::TestCase include ActiveSupport @@ -142,7 +141,7 @@ class LibXMLSAXEngineTest < ActiveSupport::TestCase morning </root> eoxml - XmlMini.parse(io) + assert_equal_rexml(io) end def test_children_with_simple_cdata @@ -185,10 +184,12 @@ class LibXMLSAXEngineTest < ActiveSupport::TestCase end private - def assert_equal_rexml(xml) - hash = XmlMini.with_backend('REXML') { XmlMini.parse(xml) } - assert_equal(hash, XmlMini.parse(xml)) - end + def assert_equal_rexml(xml) + parsed_xml = XmlMini.parse(xml) + xml.rewind if xml.respond_to?(:rewind) + hash = XmlMini.with_backend('REXML') { XmlMini.parse(xml) } + assert_equal(hash, parsed_xml) + end end end diff --git a/activesupport/test/xml_mini/nokogiri_engine_test.rb b/activesupport/test/xml_mini/nokogiri_engine_test.rb index 3f37c7cbb6..2e962576b5 100644 --- a/activesupport/test/xml_mini/nokogiri_engine_test.rb +++ b/activesupport/test/xml_mini/nokogiri_engine_test.rb @@ -1,12 +1,11 @@ -require 'abstract_unit' -require 'active_support/xml_mini' -require 'active_support/core_ext/hash/conversions' - begin require 'nokogiri' rescue LoadError # Skip nokogiri tests else +require 'abstract_unit' +require 'active_support/xml_mini' +require 'active_support/core_ext/hash/conversions' class NokogiriEngineTest < ActiveSupport::TestCase include ActiveSupport @@ -156,7 +155,7 @@ class NokogiriEngineTest < ActiveSupport::TestCase morning </root> eoxml - XmlMini.parse(io) + assert_equal_rexml(io) end def test_children_with_simple_cdata @@ -207,10 +206,12 @@ class NokogiriEngineTest < ActiveSupport::TestCase end private - def assert_equal_rexml(xml) - hash = XmlMini.with_backend('REXML') { XmlMini.parse(xml) } - assert_equal(hash, XmlMini.parse(xml)) - end + def assert_equal_rexml(xml) + parsed_xml = XmlMini.parse(xml) + xml.rewind if xml.respond_to?(:rewind) + hash = XmlMini.with_backend('REXML') { XmlMini.parse(xml) } + assert_equal(hash, parsed_xml) + end end end diff --git a/activesupport/test/xml_mini/nokogirisax_engine_test.rb b/activesupport/test/xml_mini/nokogirisax_engine_test.rb index d6ae7f12ae..4f078f31e0 100644 --- a/activesupport/test/xml_mini/nokogirisax_engine_test.rb +++ b/activesupport/test/xml_mini/nokogirisax_engine_test.rb @@ -1,12 +1,11 @@ -require 'abstract_unit' -require 'active_support/xml_mini' -require 'active_support/core_ext/hash/conversions' - begin require 'nokogiri' rescue LoadError # Skip nokogiri tests else +require 'abstract_unit' +require 'active_support/xml_mini' +require 'active_support/core_ext/hash/conversions' class NokogiriSAXEngineTest < ActiveSupport::TestCase include ActiveSupport @@ -57,9 +56,9 @@ class NokogiriSAXEngineTest < ActiveSupport::TestCase end end - def test_setting_nokogiri_as_backend - XmlMini.backend = 'Nokogiri' - assert_equal XmlMini_Nokogiri, XmlMini.backend + def test_setting_nokogirisax_as_backend + XmlMini.backend = 'NokogiriSAX' + assert_equal XmlMini_NokogiriSAX, XmlMini.backend end def test_blank_returns_empty_hash @@ -157,7 +156,7 @@ class NokogiriSAXEngineTest < ActiveSupport::TestCase morning </root> eoxml - XmlMini.parse(io) + assert_equal_rexml(io) end def test_children_with_simple_cdata @@ -208,10 +207,12 @@ class NokogiriSAXEngineTest < ActiveSupport::TestCase end private - def assert_equal_rexml(xml) - hash = XmlMini.with_backend('REXML') { XmlMini.parse(xml) } - assert_equal(hash, XmlMini.parse(xml)) - end + def assert_equal_rexml(xml) + parsed_xml = XmlMini.parse(xml) + xml.rewind if xml.respond_to?(:rewind) + hash = XmlMini.with_backend('REXML') { XmlMini.parse(xml) } + assert_equal(hash, parsed_xml) + end end end diff --git a/activesupport/test/xml_mini/rexml_engine_test.rb b/activesupport/test/xml_mini/rexml_engine_test.rb index c4770405f2..0c1f11803c 100644 --- a/activesupport/test/xml_mini/rexml_engine_test.rb +++ b/activesupport/test/xml_mini/rexml_engine_test.rb @@ -24,6 +24,14 @@ class REXMLEngineTest < ActiveSupport::TestCase morning </root> eoxml - XmlMini.parse(io) + assert_equal_rexml(io) end + + private + def assert_equal_rexml(xml) + parsed_xml = XmlMini.parse(xml) + xml.rewind if xml.respond_to?(:rewind) + hash = XmlMini.with_backend('REXML') { XmlMini.parse(xml) } + assert_equal(hash, parsed_xml) + end end diff --git a/activesupport/test/xml_mini_test.rb b/activesupport/test/xml_mini_test.rb index a025279e16..d992028323 100644 --- a/activesupport/test/xml_mini_test.rb +++ b/activesupport/test/xml_mini_test.rb @@ -106,7 +106,11 @@ module XmlMiniTest module Nokogiri end setup do - @xml = ActiveSupport::XmlMini + @xml, @default_backend = ActiveSupport::XmlMini, ActiveSupport::XmlMini.backend + end + + teardown do + ActiveSupport::XmlMini.backend = @default_backend end test "#with_backend should switch backend and then switch back" do @@ -135,7 +139,11 @@ module XmlMiniTest module LibXML end setup do - @xml = ActiveSupport::XmlMini + @xml, @default_backend = ActiveSupport::XmlMini, ActiveSupport::XmlMini.backend + end + + teardown do + ActiveSupport::XmlMini.backend = @default_backend end test "#with_backend should be thread-safe" do |