diff options
Diffstat (limited to 'activesupport')
210 files changed, 4414 insertions, 3076 deletions
diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md index adaeb99c87..f6c236bc35 100644 --- a/activesupport/CHANGELOG.md +++ b/activesupport/CHANGELOG.md @@ -1,531 +1,146 @@ -* Added `Hash#compact` and `Hash#compact!` for removing items with nil value from hash. +* Add missing time zone definitions for Russian Federation and sync them + with `zone.tab` file from tzdata version 2014j (latest). - *Celestino Gomes* + *Andrey Novikov* -* Maintain proleptic gregorian in Time#advance +* Add `SecureRandom.base58` for generation of random base58 strings. - `Time#advance` uses `Time#to_date` and `Date#advance` to calculate a new date. - The `Date` object returned by `Time#to_date` is constructed with the assumption - that the `Time` object represents a proleptic gregorian date, but it is - configured to observe the default julian calendar reform date (2299161j) - for purposes of calculating month, date and year: + *Matthew Draper*, *Guillermo Iguaran* - Time.new(1582, 10, 4).to_date.to_s # => "1582-09-24" - Time.new(1582, 10, 4).to_date.gregorian.to_s # => "1582-10-04" +* Add `#prev_day` and `#next_day` counterparts to `#yesterday` and + `#tomorrow` for `Date`, `Time`, and `DateTime`. - This patch ensures that when the intermediate `Date` object is advanced - to yield a new `Date` object, that the `Time` object for return is constructed - with a proleptic gregorian month, date and year. + *George Claghorn* - *Riley Lynch* +* Add `same_time` option to `#next_week` and `#prev_week` for `Date`, `Time`, + and `DateTime`. -* `MemCacheStore` should only accept a `Dalli::Client`, or create one. + *George Claghorn* - *arthurnn* +* Add `#on_weekend?`, `#next_weekday`, `#prev_weekday` methods to `Date`, + `Time`, and `DateTime`. -* Don't lazy load the `tzinfo` library as it causes problems on Windows. + `#on_weekend?` returns true if the receiving date/time falls on a Saturday + or Sunday. - Fixes #13553. + `#next_weekday` returns a new date/time representing the next day that does + not fall on a Saturday or Sunday. - *Andrew White* + `#prev_weekday` returns a new date/time representing the previous day that + does not fall on a Saturday or Sunday. -* Use `remove_possible_method` instead of `remove_method` to avoid - a `NameError` to be thrown on FreeBSD with the `Date` object. + *George Claghorn* - *Rafael Mendonça França*, *Robin Dupret* +* Change the default test order from `:sorted` to `:random`. -* `blank?` and `present?` commit to return singletons. + *Rafael Mendonça França* - *Xavier Noria*, *Pavel Pravosud* +* Remove deprecated `ActiveSupport::JSON::Encoding::CircularReferenceError`. -* Fixed Float related error in NumberHelper with large precisions. + *Rafael Mendonça França* - Before: +* Remove deprecated methods `ActiveSupport::JSON::Encoding.encode_big_decimal_as_string=` + and `ActiveSupport::JSON::Encoding.encode_big_decimal_as_string`. - ActiveSupport::NumberHelper.number_to_rounded '3.14159', precision: 50 - #=> "3.14158999999999988261834005243144929409027099609375" + *Rafael Mendonça França* - After: +* Remove deprecated `ActiveSupport::SafeBuffer#prepend`. - ActiveSupport::NumberHelper.number_to_rounded '3.14159', precision: 50 - #=> "3.14159000000000000000000000000000000000000000000000" + *Rafael Mendonça França* - *Kenta Murata*, *Akira Matsuda* +* Remove deprecated methods at `Kernel`. -* Default the new `I18n.enforce_available_locales` config to `true`, meaning - `I18n` will make sure that all locales passed to it must be declared in the - `available_locales` list. + `silence_stderr`, `silence_stream`, `capture` and `quietly`. - To disable it add the following configuration to your application: + *Rafael Mendonça França* - config.i18n.enforce_available_locales = false +* Remove deprecated `active_support/core_ext/big_decimal/yaml_conversions` + file. - This also ensures I18n configuration is properly initialized taking the new - option into account, to avoid their deprecations while booting up the app. + *Rafael Mendonça França* - *Carlos Antonio da Silva*, *Yves Senn* +* Remove deprecated methods `ActiveSupport::Cache::Store.instrument` and + `ActiveSupport::Cache::Store.instrument=`. -* Introduce Module#concerning: a natural, low-ceremony way to separate - responsibilities within a class. + *Rafael Mendonça França* - Imported from https://github.com/37signals/concerning#readme +* Change the way in which callback chains can be halted. - class Todo < ActiveRecord::Base - concerning :EventTracking do - included do - has_many :events - end - - def latest_event - ... - end - - private - def some_internal_method - ... - end - end - - concerning :Trashable do - def trashed? - ... - end - - def latest_event - super some_option: true - end - end - end - - is equivalent to defining these modules inline, extending them into - concerns, then mixing them in to the class. - - Inline concerns tame "junk drawer" classes that intersperse many unrelated - class-level declarations, public instance methods, and private - implementation. Coalesce related bits and give them definition. - These are a stepping stone toward future growth & refactoring. - - When to move on from an inline concern: - * Encapsulating state? Extract collaborator object. - * Encompassing more public behavior or implementation? Move to separate file. - * Sharing behavior among classes? Move to separate file. - - *Jeremy Kemper* - -* Fix file descriptor being leaked on each call to `Kernel.silence_stream`. - - *Mario Visic* - -* Added `Date#all_week/month/quarter/year` for generating date ranges. - - *Dmitriy Meremyanin* - -* Add `Time.zone.yesterday` and `Time.zone.tomorrow`. These follow the - behavior of Ruby's `Date.yesterday` and `Date.tomorrow` but return localized - versions, similar to how `Time.zone.today` has returned a localized version - of `Date.today`. - - *Colin Bartlett* - -* Show valid keys when `assert_valid_keys` raises an exception, and show the - wrong value as it was entered. - - *Gonzalo Rodríguez-Baltanás Díaz* - -* Both `cattr_*` and `mattr_*` method definitions now live in `active_support/core_ext/module/attribute_accessors`. - - Requires to `active_support/core_ext/class/attribute_accessors` are - deprecated and will be removed in Ruby on Rails 4.2. - - *Genadi Samokovarov* - -* Deprecated `Numeric#{ago,until,since,from_now}`, the user is expected to explicitly - convert the value into an AS::Duration, i.e. `5.ago` => `5.seconds.ago` - - This will help to catch subtle bugs like: - - def recent?(days = 3) - self.created_at >= days.ago - end - - The above code would check if the model is created within the last 3 **seconds**. - - In the future, `Numeric#{ago,until,since,from_now}` should be removed completely, - or throw some sort of errors to indicate there are no implicit conversion from - Numeric to AS::Duration. - - *Godfrey Chan* - -* Requires JSON gem version 1.7.7 or above due to a security issue in older versions. - - *Godfrey Chan* - -* Removed the old pure-Ruby JSON encoder and switched to a new encoder based on the built-in JSON - gem. - - Support for encoding `BigDecimal` as a JSON number, as well as defining custom `encode_json` - methods to control the JSON output has been **removed from core**. The new encoder will always - encode BigDecimals as `String`s and ignore any custom `encode_json` methods. - - The old encoder has been extracted into the `activesupport-json_encoder` gem. Installing that - gem will bring back the ability to encode `BigDecimal`s as numbers as well as `encode_json` - support. - - Setting the related configuration `ActiveSupport.encode_big_decimal_as_string` without the - `activesupport-json_encoder` gem installed will raise an error. - - *Godfrey Chan* - -* Add `ActiveSupport::Testing::TimeHelpers#travel` and `#travel_to`. These methods change current - time to the given time or time difference by stubbing `Time.now` and `Date.today` to return the - time or date after the difference calculation, or the time or date that got passed into the - method respectively. - - Example for `#travel`: + The preferred method to halt a callback chain from now on is to explicitly + `throw(:abort)`. + In the past, returning `false` in an ActiveSupport callback had the side + effect of halting the callback chain. This is not recommended anymore and, + depending on the value of + `Callbacks::CallbackChain.halt_and_display_warning_on_return_false`, will + either not work at all or display a deprecation warning. - Time.now # => 2013-11-09 15:34:49 -05:00 - travel 1.day - Time.now # => 2013-11-10 15:34:49 -05:00 - Date.today # => Sun, 10 Nov 2013 +* Add Callbacks::CallbackChain.halt_and_display_warning_on_return_false - Example for `#travel_to`: + Setting `Callbacks::CallbackChain.halt_and_display_warning_on_return_false` + to true will let an app support the deprecated way of halting callback + chains by returning `false`. - Time.now # => 2013-11-09 15:34:49 -05:00 - travel_to Time.new(2004, 11, 24, 01, 04, 44) - Time.now # => 2004-11-24 01:04:44 -05:00 - Date.today # => Wed, 24 Nov 2004 + Setting the value to false will tell the app to ignore any `false` value + returned by callbacks, and only halt the chain upon `throw(:abort)`. - Both of these methods also accept a block, which will return the current time back to its - original state at the end of the block: + The value can also be set with the Rails configuration option + `config.active_support.halt_callback_chains_on_return_false`. - Time.now # => 2013-11-09 15:34:49 -05:00 - - travel 1.day do - User.create.created_at # => Sun, 10 Nov 2013 15:34:49 EST -05:00 - end - - travel_to Time.new(2004, 11, 24, 01, 04, 44) do - User.create.created_at # => Wed, 24 Nov 2004 01:04:44 EST -05:00 - end - - Time.now # => 2013-11-09 15:34:49 -05:00 - - This module is included in `ActiveSupport::TestCase` automatically. - - *Prem Sichanugrist*, *DHH* - -* Unify `cattr_*` interface: allow to pass a block to `cattr_reader`. - - Example: - - class A - cattr_reader(:defr) { 'default_reader_value' } - end - A.defr # => 'default_reader_value' - - *Alexey Chernenkov* - -* Improved compatibility with the stdlib JSON gem. - - Previously, calling `::JSON.{generate,dump}` sometimes causes unexpected - failures such as intridea/multi_json#86. - - `::JSON.{generate,dump}` now bypasses the ActiveSupport JSON encoder - completely and yields the same result with or without ActiveSupport. This - means that it will **not** call `as_json` and will ignore any options that - the JSON gem does not natively understand. To invoke ActiveSupport's JSON - encoder instead, use `obj.to_json(options)` or - `ActiveSupport::JSON.encode(obj, options)`. - - *Godfrey Chan* - -* Fix Active Support `Time#to_json` and `DateTime#to_json` to return 3 decimal - places worth of fractional seconds, similar to `TimeWithZone`. - - *Ryan Glover* - -* Removed circular reference protection in JSON encoder, deprecated - `ActiveSupport::JSON::Encoding::CircularReferenceError`. - - *Godfrey Chan*, *Sergio Campamá* - -* Add `capitalize` option to `Inflector.humanize`, so strings can be humanized without being capitalized: - - 'employee_salary'.humanize # => "Employee salary" - 'employee_salary'.humanize(capitalize: false) # => "employee salary" + When the configuration option is missing, its value is `true`, so older apps + ported to Rails 5.0 will not break (but display a deprecation warning). + For new Rails 5.0 apps, its value is set to `false` in an initializer, so + these apps will support the new behavior by default. *claudiob* -* Fixed `Object#as_json` and `Struct#as_json` not working properly with options. They now take - the same options as `Hash#as_json`: - - struct = Struct.new(:foo, :bar).new - struct.foo = "hello" - struct.bar = "world" - json = struct.as_json(only: [:foo]) # => {foo: "hello"} - - *Sergio Campamá*, *Godfrey Chan* - -* Added `Numeric#in_milliseconds`, like `1.hour.in_milliseconds`, so we can feed them to JavaScript functions like `getTime()`. - - *DHH* +* Changes arguments and default value of CallbackChain's :terminator option -* Calling `ActiveSupport::JSON.decode` with unsupported options now raises an error. + Chains of callbacks defined without an explicit `:terminator` option will + now be halted as soon as a `before_` callback throws `:abort`. - *Godfrey Chan* + Chains of callbacks defined with a `:terminator` option will maintain their + existing behavior of halting as soon as a `before_` callback matches the + terminator's expectation. -* Support `:unless_exist` in `FileStore`. - - *Michael Grosser* - -* Fix `slice!` deleting the default value of the hash. - - *Antonio Santos* + *claudiob* -* `require_dependency` accepts objects that respond to `to_path`, in - particular `Pathname` instances. +* Deprecate `MissingSourceFile` in favor of `LoadError`. - *Benjamin Fleischer* + `MissingSourceFile` was just an alias to `LoadError` and was not being + raised inside the framework. -* Disable the ability to iterate over Range of AS::TimeWithZone - due to significant performance issues. + *Rafael Mendonça França* - *Bogdan Gusiev* +* Add support for error dispatcher classes in `ActiveSupport::Rescuable`. + Now it acts closer to Ruby's rescue. -* Allow attaching event subscribers to ActiveSupport::Notifications namespaces - before they're defined. Essentially, this means instead of this: + Example: - class JokeSubscriber < ActiveSupport::Subscriber - def sql(event) - puts "A rabbi and a priest walk into a bar..." + class BaseController < ApplicationController + module ErrorDispatcher + def self.===(other) + Exception === other && other.respond_to?(:status) + end end - # This call needs to happen *after* defining the methods. - attach_to "active_record" - end - - You can do this: - - class JokeSubscriber < ActiveSupport::Subscriber - # This is much easier to read! - attach_to "active_record" - - def sql(event) - puts "A rabbi and a priest walk into a bar..." + rescue_from ErrorDispatcher do |error| + render status: error.status, json: { error: error.to_s } end end - This should make it easier to read and understand these subscribers. - - *Daniel Schierbeck* - -* Add `Date#middle_of_day`, `DateTime#middle_of_day` and `Time#middle_of_day` methods. - - Also added `midday`, `noon`, `at_midday`, `at_noon` and `at_middle_of_day` as aliases. - - *Anatoli Makarevich* - -* Fix ActiveSupport::Cache::FileStore#cleanup to no longer rely on missing each_key method. - - *Murray Steele* - -* Ensure that autoloaded constants in all-caps nestings are marked as - autoloaded. - - *Simon Coffey* - -* Add `String#remove(pattern)` as a short-hand for the common pattern of - `String#gsub(pattern, '')`. - - *DHH* - -* Adds a new deprecation behaviour that raises an exception. Throwing this - line into +config/environments/development.rb+ - - ActiveSupport::Deprecation.behavior = :raise - - will cause the application to raise an +ActiveSupport::DeprecationException+ - on deprecations. - - Use this for aggressive deprecation cleanups. - - *Xavier Noria* - -* Remove 'cow' => 'kine' irregular inflection from default inflections. - - *Andrew White* - -* Add `DateTime#to_s(:iso8601)` and `Date#to_s(:iso8601)` for consistency. - - *Andrew White* - -* Add `Time#to_s(:iso8601)` for easy conversion of times to the iso8601 format for easy Javascript date parsing. - - *DHH* - -* Improve `ActiveSupport::Cache::MemoryStore` cache size calculation. - The memory used by a key/entry pair is calculated via `#cached_size`: - - def cached_size(key, entry) - key.to_s.bytesize + entry.size + PER_ENTRY_OVERHEAD - end - - 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. - - Fixes #11512. - - *Simeon Simeonov* - -* Only raise `Module::DelegationError` if it's the source of the exception. - - Fixes #10559. - - *Andrew White* - -* Make `Time.at_with_coercion` retain the second fraction and return local time. - - Fixes #11350. - - *Neer Friedman*, *Andrew White* - -* Make `HashWithIndifferentAccess#select` always return the hash, even when - `Hash#select!` returns `nil`, to allow further chaining. - - *Marc Schütz* - -* Remove deprecated `String#encoding_aware?` core extensions (`core_ext/string/encoding`). - - *Arun Agrawal* - -* Remove deprecated `Module#local_constant_names` in favor of `Module#local_constants`. - - *Arun Agrawal* - -* Remove deprecated `DateTime.local_offset` in favor of `DateTime.civil_from_format`. - - *Arun Agrawal* - -* Remove deprecated `Logger` core extensions (`core_ext/logger.rb`). - - *Carlos Antonio da Silva* - -* Remove deprecated `Time#time_with_datetime_fallback`, `Time#utc_time` - and `Time#local_time` in favor of `Time#utc` and `Time#local`. - - *Vipul A M* - -* Remove deprecated `Hash#diff` with no replacement. - - If you're using it to compare hashes for the purpose of testing, please use - MiniTest's `assert_equal` instead. - - *Carlos Antonio da Silva* - -* Remove deprecated `Date#to_time_in_current_zone` in favor of `Date#in_time_zone`. - - *Vipul A M* - -* Remove deprecated `Proc#bind` with no replacement. - - *Carlos Antonio da Silva* - -* Remove deprecated `Array#uniq_by` and `Array#uniq_by!`, use native - `Array#uniq` and `Array#uniq!` instead. - - *Carlos Antonio da Silva* - -* Remove deprecated `ActiveSupport::BasicObject`, use `ActiveSupport::ProxyObject` instead. - - *Carlos Antonio da Silva* - -* Remove deprecated `BufferedLogger`, use `ActiveSupport::Logger` instead. - - *Yves Senn* - -* Remove deprecated `assert_present` and `assert_blank` methods, use `assert - object.blank?` and `assert object.present?` instead. - - *Yves Senn* - -* Fix return value from `BacktraceCleaner#noise` when the cleaner is configured - with multiple silencers. - - Fixes #11030. - - *Mark J. Titorenko* - -* `HashWithIndifferentAccess#select` now returns a `HashWithIndifferentAccess` - instance instead of a `Hash` instance. - - Fixes #10723. - - *Albert Llop* - -* Add `DateTime#usec` and `DateTime#nsec` so that `ActiveSupport::TimeWithZone` keeps - sub-second resolution when wrapping a `DateTime` value. - - Fixes #10855. - - *Andrew White* - -* Fix `ActiveSupport::Dependencies::Loadable#load_dependency` calling - `#blame_file!` on Exceptions that do not have the Blamable mixin - - *Andrew Kreiling* - -* Override `Time.at` to support the passing of Time-like values when called with a single argument. - - *Andrew White* - -* Prevent side effects to hashes inside arrays when - `Hash#with_indifferent_access` is called. - - Fixes #10526. - - *Yves Senn* - -* Removed deprecated `ActiveSupport::JSON::Variable` with no replacement. - - *Toshinori Kajihara* - -* 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. - - *Mike Dillon* - -* Replace `multi_json` with `json`. - - Since Rails requires Ruby 1.9 and since Ruby 1.9 includes `json` in the standard library, - `multi_json` is no longer necessary. - - *Erik Michaels-Ober* - -* 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. - - *Mario Caropreso + Viktor Kelemen + zackham* - -* Fix skipping object callbacks using metadata fetched via callback chain - inspection methods (`_*_callbacks`) + *Genadi Samokovarov* - *Sean Walbran* +* Add `#verified` and `#valid_message?` methods to `ActiveSupport::MessageVerifier` -* Add a `fetch_multi` method to the cache stores. The method provides - an easy to use API for fetching multiple values from the cache. + Previously, the only way to decode a message with `ActiveSupport::MessageVerifier` + was to use `#verify`, which would raise an exception on invalid messages. Now + `#verified` can also be used, which returns `nil` on messages that cannot be + decoded. - Example: - - # 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 + Previously, there was no way to check if a message's format was valid without + attempting to decode it. `#valid_message?` is a boolean convenience method that + checks whether the message is valid without actually decoding it. - *Daniel Schierbeck* + *Logan Leger* -Please check [4-0-stable](https://github.com/rails/rails/blob/4-0-stable/activesupport/CHANGELOG.md) for previous changes. +Please check [4-2-stable](https://github.com/rails/rails/blob/4-2-stable/activesupport/CHANGELOG.md) for previous changes. diff --git a/activesupport/MIT-LICENSE b/activesupport/MIT-LICENSE index d06d4f3b2d..7bffebb076 100644 --- a/activesupport/MIT-LICENSE +++ b/activesupport/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2005-2014 David Heinemeier Hansson +Copyright (c) 2005-2015 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 f3582767c0..a6424a353a 100644 --- a/activesupport/README.rdoc +++ b/activesupport/README.rdoc @@ -30,6 +30,11 @@ API documentation is at: * http://api.rubyonrails.org -Bug reports and feature requests can be filed with the rest for the Ruby on Rails project here: +Bug reports can be filed for the Ruby on Rails project here: * https://github.com/rails/rails/issues + +Feature requests should be discussed on the rails-core mailing list here: + +* https://groups.google.com/forum/?fromgroups#!forum/rubyonrails-core + diff --git a/activesupport/Rakefile b/activesupport/Rakefile index 5ba153662a..7c40df8dc8 100644 --- a/activesupport/Rakefile +++ b/activesupport/Rakefile @@ -7,9 +7,9 @@ Rake::TestTask.new do |t| t.pattern = 'test/**/*_test.rb' t.warning = true t.verbose = true + t.ruby_opts = ["--dev"] if defined?(JRUBY_VERSION) end - namespace :test do task :isolated do Dir.glob("test/**/*_test.rb").all? do |file| diff --git a/activesupport/activesupport.gemspec b/activesupport/activesupport.gemspec index f3625e8b79..a5339e6475 100644 --- a/activesupport/activesupport.gemspec +++ b/activesupport/activesupport.gemspec @@ -7,7 +7,7 @@ Gem::Specification.new do |s| s.summary = 'A toolkit of support libraries and Ruby core extensions extracted from the Rails framework.' s.description = 'A toolkit of support libraries and Ruby core extensions extracted from the Rails framework. Rich support for multibyte strings, internationalization, time zones, and testing.' - s.required_ruby_version = '>= 1.9.3' + s.required_ruby_version = '>= 2.2.0' s.license = 'MIT' @@ -20,9 +20,9 @@ Gem::Specification.new do |s| s.rdoc_options.concat ['--encoding', 'UTF-8'] - s.add_dependency 'i18n', '~> 0.6', '>= 0.6.9' + s.add_dependency 'i18n', '~> 0.7' s.add_dependency 'json', '~> 1.7', '>= 1.7.7' s.add_dependency 'tzinfo', '~> 1.1' s.add_dependency 'minitest', '~> 5.1' - s.add_dependency 'thread_safe','~> 0.1' + s.add_dependency 'thread_safe','~> 0.3', '>= 0.3.4' end diff --git a/activesupport/bin/generate_tables b/activesupport/bin/generate_tables index f39e89b7d0..71a6b78652 100755 --- a/activesupport/bin/generate_tables +++ b/activesupport/bin/generate_tables @@ -55,7 +55,7 @@ module ActiveSupport codepoint.combining_class = Integer($4) #codepoint.bidi_class = $5 codepoint.decomp_type = $7 - codepoint.decomp_mapping = ($8=='') ? nil : $8.split.collect { |element| element.hex } + codepoint.decomp_mapping = ($8=='') ? nil : $8.split.collect(&:hex) #codepoint.bidi_mirrored = ($13=='Y') ? true : false codepoint.uppercase_mapping = ($16=='') ? 0 : $16.hex codepoint.lowercase_mapping = ($17=='') ? 0 : $17.hex diff --git a/activesupport/lib/active_support.rb b/activesupport/lib/active_support.rb index ab0054b339..290920dbf8 100644 --- a/activesupport/lib/active_support.rb +++ b/activesupport/lib/active_support.rb @@ -1,5 +1,5 @@ #-- -# Copyright (c) 2005-2014 David Heinemeier Hansson +# Copyright (c) 2005-2015 David Heinemeier Hansson # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -70,6 +70,8 @@ module ActiveSupport NumberHelper.eager_load! end + + cattr_accessor :test_order # :nodoc: end autoload :I18n, "active_support/i18n" diff --git a/activesupport/lib/active_support/backtrace_cleaner.rb b/activesupport/lib/active_support/backtrace_cleaner.rb index d58578b7bc..d06f22ad5c 100644 --- a/activesupport/lib/active_support/backtrace_cleaner.rb +++ b/activesupport/lib/active_support/backtrace_cleaner.rb @@ -13,7 +13,7 @@ module ActiveSupport # can focus on the rest. # # bc = BacktraceCleaner.new - # bc.add_filter { |line| line.gsub(Rails.root, '') } # strip the Rails.root prefix + # bc.add_filter { |line| line.gsub(Rails.root.to_s, '') } # 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 # @@ -65,14 +65,14 @@ module ActiveSupport @silencers << block end - # Will remove all silencers, but leave in the filters. This is useful if - # your context of debugging suddenly expands as you suspect a bug in one of + # Removes all silencers, but leaves in the filters. Useful if your + # context of debugging suddenly expands as you suspect a bug in one of # the libraries you use. def remove_silencers! @silencers = [] end - # Removes all filters, but leaves in silencers. Useful if you suddenly + # Removes all filters, but leaves in the silencers. Useful if you suddenly # need to see entire filepaths in the backtrace that you had already # filtered out. def remove_filters! diff --git a/activesupport/lib/active_support/cache.rb b/activesupport/lib/active_support/cache.rb index 53154aef27..3a1e1ac3a1 100644 --- a/activesupport/lib/active_support/cache.rb +++ b/activesupport/lib/active_support/cache.rb @@ -178,16 +178,6 @@ module ActiveSupport @silence = previous_silence end - # Set to +true+ if cache stores should be instrumented. - # Default is +false+. - def self.instrument=(boolean) - Thread.current[:instrument_cache_store] = boolean - end - - def self.instrument - Thread.current[:instrument_cache_store] || false - end - # Fetches data from the cache, using the given key. If there is data in # the cache with the given key, then that data is returned. # @@ -234,7 +224,7 @@ module ActiveSupport # seconds. Because of extended life of the previous cache, other processes # 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. + # new value. After that all the processes will start getting the new value. # The key is to keep <tt>:race_condition_ttl</tt> small. # # If the process regenerating the entry errors out, the entry will be @@ -357,20 +347,19 @@ module ActiveSupport # # Options are passed to the underlying cache implementation. # - # Returns an array with the data for each of the names. For example: + # Returns a hash with the data for each of the names. For example: # # cache.write("bim", "bam") - # cache.fetch_multi("bim", "boom") {|key| key * 2 } - # # => ["bam", "boomboom"] + # cache.fetch_multi("bim", "boom") { |key| key * 2 } + # # => { "bam" => "bam", "boom" => "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 + names.each_with_object({}) do |name, memo| + memo[name] = results.fetch(name) do value = yield name write(name, value, options) value @@ -452,7 +441,7 @@ module ActiveSupport # Clear the entire cache. Be careful with this method since it could # affect other processes if shared cache is being used. # - # Options are passed to the underlying cache implementation. + # The options hash is passed to the underlying cache implementation. # # All implementations may not support this method. def clear(options = nil) @@ -540,13 +529,9 @@ module ActiveSupport def instrument(operation, key, options = nil) log(operation, key, options) - if self.class.instrument - payload = { :key => key } - payload.merge!(options) if options.is_a?(Hash) - ActiveSupport::Notifications.instrument("cache_#{operation}.active_support", payload){ yield(payload) } - else - yield(nil) - end + payload = { :key => key } + payload.merge!(options) if options.is_a?(Hash) + ActiveSupport::Notifications.instrument("cache_#{operation}.active_support", payload){ yield(payload) } end def log(operation, key, options = nil) @@ -617,14 +602,12 @@ module ActiveSupport end def value - 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_4beta1_entry! if defined?(@value) @expires_in && @created_at + @expires_in <= Time.now.to_f end @@ -660,8 +643,6 @@ 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_4beta1_entry! if defined?(@v) - if @value && !compressed? && !(@value.is_a?(Numeric) || @value == true || @value == false) if @value.is_a?(String) @value = @value.dup @@ -694,26 +675,6 @@ module ActiveSupport def uncompress(value) Marshal.load(Zlib::Inflate.inflate(value)) end - - # 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_4beta1_entry! - if defined?(@v) - @value = @v - remove_instance_variable(:@v) - end - - if defined?(@c) - @compressed = @c - remove_instance_variable(:@c) - end - - if defined?(@x) && @x - @created_at ||= Time.now.to_f - @expires_in = @x - @created_at - remove_instance_variable(:@x) - end - end end end end diff --git a/activesupport/lib/active_support/cache/file_store.rb b/activesupport/lib/active_support/cache/file_store.rb index 8ed60aebac..d08ecd2f7d 100644 --- a/activesupport/lib/active_support/cache/file_store.rb +++ b/activesupport/lib/active_support/cache/file_store.rb @@ -14,6 +14,7 @@ module ActiveSupport DIR_FORMATTER = "%03X" FILENAME_MAX_SIZE = 228 # max filename size on file system is 255, minus room for timestamp and random characters appended by Tempfile (used by atomic write) + FILEPATH_MAX_SIZE = 900 # max is 1024, plus some room EXCLUDED_DIRS = ['.', '..'].freeze def initialize(cache_path, options = nil) @@ -117,6 +118,10 @@ module ActiveSupport # Translate a key into a file path. def key_file_path(key) + if key.size > FILEPATH_MAX_SIZE + key = Digest::MD5.hexdigest(key) + end + fname = URI.encode_www_form_component(key) hash = Zlib.adler32(fname) hash, dir_1 = hash.divmod(0x1000) diff --git a/activesupport/lib/active_support/cache/strategy/local_cache.rb b/activesupport/lib/active_support/cache/strategy/local_cache.rb index 4eaf57f385..73c6b3cb88 100644 --- a/activesupport/lib/active_support/cache/strategy/local_cache.rb +++ b/activesupport/lib/active_support/cache/strategy/local_cache.rb @@ -1,6 +1,6 @@ require 'active_support/core_ext/object/duplicable' require 'active_support/core_ext/string/inflections' -require 'rack/body_proxy' +require 'active_support/per_thread_registry' module ActiveSupport module Cache @@ -9,6 +9,8 @@ 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 + autoload :Middleware, 'active_support/cache/strategy/local_cache_middleware' + # Class for storing and registering the local caches. class LocalCacheRegistry # :nodoc: extend ActiveSupport::PerThreadRegistry @@ -64,37 +66,6 @@ module ActiveSupport def with_local_cache 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, :local_cache_key - - def initialize(name, local_cache_key) - @name = name - @local_cache_key = local_cache_key - @app = nil - end - - def new(app) - @app = app - self - end - - def call(env) - LocalCacheRegistry.set_cache_for(local_cache_key, LocalStore.new) - response = @app.call(env) - response[2] = ::Rack::BodyProxy.new(response[2]) do - LocalCacheRegistry.set_cache_for(local_cache_key, nil) - end - response - rescue Exception - LocalCacheRegistry.set_cache_for(local_cache_key, nil) - raise - end - end - # Middleware class can be inserted as a Rack handler to be local cache for the # duration of request. def middleware @@ -115,13 +86,13 @@ module ActiveSupport def increment(name, amount = 1, options = nil) # :nodoc: value = bypass_local_cache{super} - increment_or_decrement(value, name, amount, options) + set_cache_value(value, name, amount, options) value end def decrement(name, amount = 1, options = nil) # :nodoc: value = bypass_local_cache{super} - increment_or_decrement(value, name, amount, options) + set_cache_value(value, name, amount, options) value end @@ -149,8 +120,7 @@ module ActiveSupport super end - private - def increment_or_decrement(value, name, amount, options) + def set_cache_value(value, name, amount, options) if local_cache local_cache.mute do if value @@ -162,6 +132,8 @@ module ActiveSupport end end + private + def local_cache_key @local_cache_key ||= "#{self.class.name.underscore}_local_cache_#{object_id}".gsub(/[\/-]/, '_').to_sym end diff --git a/activesupport/lib/active_support/cache/strategy/local_cache_middleware.rb b/activesupport/lib/active_support/cache/strategy/local_cache_middleware.rb new file mode 100644 index 0000000000..a6f24b1a3c --- /dev/null +++ b/activesupport/lib/active_support/cache/strategy/local_cache_middleware.rb @@ -0,0 +1,44 @@ +require 'rack/body_proxy' +require 'rack/utils' + +module ActiveSupport + module Cache + module Strategy + module LocalCache + + #-- + # This class wraps up local storage for middlewares. Only the middleware method should + # construct them. + class Middleware # :nodoc: + attr_reader :name, :local_cache_key + + def initialize(name, local_cache_key) + @name = name + @local_cache_key = local_cache_key + @app = nil + end + + def new(app) + @app = app + self + end + + def call(env) + LocalCacheRegistry.set_cache_for(local_cache_key, LocalStore.new) + response = @app.call(env) + response[2] = ::Rack::BodyProxy.new(response[2]) do + LocalCacheRegistry.set_cache_for(local_cache_key, nil) + end + response + rescue Rack::Utils::InvalidParameterError + LocalCacheRegistry.set_cache_for(local_cache_key, nil) + [400, {}, []] + rescue Exception + LocalCacheRegistry.set_cache_for(local_cache_key, nil) + raise + end + end + end + end + end +end diff --git a/activesupport/lib/active_support/callbacks.rb b/activesupport/lib/active_support/callbacks.rb index e14ece7f35..0f1de8b076 100644 --- a/activesupport/lib/active_support/callbacks.rb +++ b/activesupport/lib/active_support/callbacks.rb @@ -4,6 +4,7 @@ require 'active_support/core_ext/array/extract_options' require 'active_support/core_ext/class/attribute' require 'active_support/core_ext/kernel/reporting' require 'active_support/core_ext/kernel/singleton_class' +require 'active_support/core_ext/string/filters' require 'thread' module ActiveSupport @@ -71,24 +72,28 @@ module ActiveSupport # order. # # If the callback chain was halted, returns +false+. Otherwise returns the - # result of the block, or +true+ if no block is given. + # result of the block, +nil+ if no callbacks have been set, or +true+ + # if callbacks have been set but no block is given. # # run_callbacks :save do # save # end def run_callbacks(kind, &block) - cbs = send("_#{kind}_callbacks") - if cbs.empty? - yield if block_given? + send "_run_#{kind}_callbacks", &block + end + + private + + def _run_callbacks(callbacks, &block) + if callbacks.empty? + block.call if block else - runner = cbs.compile + runner = callbacks.compile e = Filters::Environment.new(self, false, nil, block) runner.call(e).value end end - private - # A hook invoked every time a before callback is halted. # This can be overridden in AS::Callback implementors in order # to provide better debugging/logging. @@ -117,102 +122,106 @@ module ActiveSupport ENDING = End.new class Before - def self.build(next_callback, user_callback, user_conditions, chain_config, filter) + def self.build(callback_sequence, 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) + halting_and_conditional(callback_sequence, user_callback, user_conditions, halted_lambda, filter) elsif chain_config.key? :terminator - halting(next_callback, user_callback, halted_lambda, filter) + halting(callback_sequence, user_callback, halted_lambda, filter) elsif user_conditions.any? - conditional(next_callback, user_callback, user_conditions) + conditional(callback_sequence, user_callback, user_conditions) else - simple next_callback, user_callback + simple callback_sequence, user_callback end end - private - - def self.halting_and_conditional(next_callback, user_callback, user_conditions, halted_lambda, filter) - lambda { |env| + def self.halting_and_conditional(callback_sequence, user_callback, user_conditions, halted_lambda, filter) + callback_sequence.before do |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) + result_lambda = -> { user_callback.call target, value } + env.halted = halted_lambda.call(target, result_lambda) if env.halted target.send :halted_callback_hook, filter end end - next_callback.call env - } + + env + end end + private_class_method :halting_and_conditional - def self.halting(next_callback, user_callback, halted_lambda, filter) - lambda { |env| + def self.halting(callback_sequence, user_callback, halted_lambda, filter) + callback_sequence.before do |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) + result_lambda = -> { user_callback.call target, value } + env.halted = halted_lambda.call(target, result_lambda) + if env.halted target.send :halted_callback_hook, filter end end - next_callback.call env - } + + env + end end + private_class_method :halting - def self.conditional(next_callback, user_callback, user_conditions) - lambda { |env| + def self.conditional(callback_sequence, user_callback, user_conditions) + callback_sequence.before do |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 - } + + env + end end + private_class_method :conditional - def self.simple(next_callback, user_callback) - lambda { |env| + def self.simple(callback_sequence, user_callback) + callback_sequence.before do |env| user_callback.call env.target, env.value - next_callback.call env - } + + env + end end + private_class_method :simple end class After - def self.build(next_callback, user_callback, user_conditions, chain_config) + def self.build(callback_sequence, 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) + halting_and_conditional(callback_sequence, user_callback, user_conditions) elsif chain_config.key?(:terminator) - halting(next_callback, user_callback) + halting(callback_sequence, user_callback) elsif user_conditions.any? - conditional next_callback, user_callback, user_conditions + conditional callback_sequence, user_callback, user_conditions else - simple next_callback, user_callback + simple callback_sequence, user_callback end else if user_conditions.any? - conditional next_callback, user_callback, user_conditions + conditional callback_sequence, user_callback, user_conditions else - simple next_callback, user_callback + simple callback_sequence, user_callback end end end - private - - def self.halting_and_conditional(next_callback, user_callback, user_conditions) - lambda { |env| - env = next_callback.call env + def self.halting_and_conditional(callback_sequence, user_callback, user_conditions) + callback_sequence.after do |env| target = env.target value = env.value halted = env.halted @@ -220,118 +229,126 @@ module ActiveSupport if !halted && user_conditions.all? { |c| c.call(target, value) } user_callback.call target, value end + env - } + end end + private_class_method :halting_and_conditional - def self.halting(next_callback, user_callback) - lambda { |env| - env = next_callback.call env + def self.halting(callback_sequence, user_callback) + callback_sequence.after do |env| unless env.halted user_callback.call env.target, env.value end + env - } + end end + private_class_method :halting - def self.conditional(next_callback, user_callback, user_conditions) - lambda { |env| - env = next_callback.call env + def self.conditional(callback_sequence, user_callback, user_conditions) + callback_sequence.after do |env| target = env.target value = env.value if user_conditions.all? { |c| c.call(target, value) } user_callback.call target, value end + env - } + end end + private_class_method :conditional - def self.simple(next_callback, user_callback) - lambda { |env| - env = next_callback.call env + def self.simple(callback_sequence, user_callback) + callback_sequence.after do |env| user_callback.call env.target, env.value + env - } + end end + private_class_method :simple end class Around - def self.build(next_callback, user_callback, user_conditions, chain_config) + def self.build(callback_sequence, user_callback, user_conditions, chain_config) if chain_config.key?(:terminator) && user_conditions.any? - halting_and_conditional(next_callback, user_callback, user_conditions) + halting_and_conditional(callback_sequence, user_callback, user_conditions) elsif chain_config.key? :terminator - halting(next_callback, user_callback) + halting(callback_sequence, user_callback) elsif user_conditions.any? - conditional(next_callback, user_callback, user_conditions) + conditional(callback_sequence, user_callback, user_conditions) else - simple(next_callback, user_callback) + simple(callback_sequence, user_callback) end end - private - - def self.halting_and_conditional(next_callback, user_callback, user_conditions) - lambda { |env| + def self.halting_and_conditional(callback_sequence, user_callback, user_conditions) + callback_sequence.around do |env, &run| 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 = run.call env env.value } + env else - next_callback.call env + run.call env end - } + end end + private_class_method :halting_and_conditional - def self.halting(next_callback, user_callback) - lambda { |env| + def self.halting(callback_sequence, user_callback) + callback_sequence.around do |env, &run| target = env.target value = env.value - unless env.halted + if env.halted + run.call env + else user_callback.call(target, value) { - env = next_callback.call env + env = run.call env env.value } env - else - next_callback.call env end - } + end end + private_class_method :halting - def self.conditional(next_callback, user_callback, user_conditions) - lambda { |env| + def self.conditional(callback_sequence, user_callback, user_conditions) + callback_sequence.around do |env, &run| 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 = run.call env env.value } env else - next_callback.call env + run.call env end - } + end end + private_class_method :conditional - def self.simple(next_callback, user_callback) - lambda { |env| + def self.simple(callback_sequence, user_callback) + callback_sequence.around do |env, &run| user_callback.call(env.target, env.value) { - env = next_callback.call env + env = run.call env env.value } env - } + end end + private_class_method :simple end end @@ -382,17 +399,17 @@ module ActiveSupport end # Wraps code with filter - def apply(next_callback) + def apply(callback_sequence) user_conditions = conditions_lambdas user_callback = make_lambda @filter case kind when :before - Filters::Before.build(next_callback, user_callback, user_conditions, chain_config, @filter) + Filters::Before.build(callback_sequence, user_callback, user_conditions, chain_config, @filter) when :after - Filters::After.build(next_callback, user_callback, user_conditions, chain_config) + Filters::After.build(callback_sequence, user_callback, user_conditions, chain_config) when :around - Filters::Around.build(next_callback, user_callback, user_conditions, chain_config) + Filters::Around.build(callback_sequence, user_callback, user_conditions, chain_config) end end @@ -409,15 +426,8 @@ module ActiveSupport # Procs:: A proc to call with the object. # Objects:: An object with a <tt>before_foo</tt> method on it to call. # - # All of these objects are compiled into methods and handled - # the same after this point: - # - # Symbols:: Already 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. + # All of these objects are converted into a lambda and handled + # the same after this point. def make_lambda(filter) case filter when Symbol @@ -464,16 +474,59 @@ module ActiveSupport end end + # Execute before and after filters in a sequence instead of + # chaining them with nested lambda calls, see: + # https://github.com/rails/rails/issues/18011 + class CallbackSequence + def initialize(&call) + @call = call + @before = [] + @after = [] + end + + def before(&before) + @before.unshift(before) + self + end + + def after(&after) + @after.push(after) + self + end + + def around(&around) + CallbackSequence.new do |*args| + around.call(*args) { + self.call(*args) + } + end + end + + def call(*args) + @before.each { |b| b.call(*args) } + value = @call.call(*args) + @after.each { |a| a.call(*args) } + value + end + end + # An Array with a compile method. class CallbackChain #:nodoc:# include Enumerable attr_reader :name, :config + # If true, any callback returning +false+ will halt the entire callback + # chain and display a deprecation message. If false, callback chains will + # only be halted by calling +throw :abort+. Defaults to +true+. + class_attribute :halt_and_display_warning_on_return_false + self.halt_and_display_warning_on_return_false = true + def initialize(name, config) @name = name @config = { - :scope => [ :kind ] + scope: [:kind], + terminator: default_terminator }.merge!(config) @chain = [] @callbacks = nil @@ -508,8 +561,9 @@ module ActiveSupport def compile @callbacks || @mutex.synchronize do - @callbacks ||= @chain.reverse.inject(Filters::ENDING) do |chain, callback| - callback.apply chain + final_sequence = CallbackSequence.new { |env| Filters::ENDING.call(env) } + @callbacks ||= @chain.reverse.inject(final_sequence) do |callback_sequence, callback| + callback.apply callback_sequence end end end @@ -543,6 +597,28 @@ module ActiveSupport @callbacks = nil @chain.delete_if { |c| callback.duplicates?(c) } end + + def default_terminator + Proc.new do |target, result_lambda| + terminate = true + catch(:abort) do + result = result_lambda.call if result_lambda.is_a?(Proc) + if halt_and_display_warning_on_return_false && result == false + display_deprecation_warning_for_false_terminator + else + terminate = false + end + end + terminate + end + end + + def display_deprecation_warning_for_false_terminator + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Returning `false` in a callback will not implicitly halt a callback chain in the next release of Rails. + To explicitly halt a callback chain, please use `throw :abort` instead. + MSG + end end module ClassMethods @@ -556,7 +632,7 @@ module ActiveSupport # 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| + ([self] + ActiveSupport::DescendantsTracker.descendants(self)).reverse_each do |target| chain = target.get_callbacks name yield target, chain.dup end @@ -566,7 +642,7 @@ module ActiveSupport # # set_callback :save, :before, :before_meth # set_callback :save, :after, :after_meth, if: :condition - # set_callback :save, :around, ->(r, &block) { stuff; result = block.call; stuff } + # set_callback :save, :around, ->(r, block) { stuff; result = block.call; stuff } # # The second arguments indicates whether the callback is to be run +:before+, # +:after+, or +:around+ the event. If omitted, +:before+ is assumed. This @@ -656,16 +732,17 @@ module ActiveSupport # ===== Options # # * <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 should be a lambda to be executed. + # callback chain, preventing following before and around callbacks from + # being called and 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: ->(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 - # halts the chain. + # any successive before and around callback is not executed. + # Defaults to +false+, meaning no value halts the chain. # # * <tt>:skip_after_callbacks_if_terminated</tt> - Determines if after # callbacks should be terminated by the <tt>:terminator</tt> option. By @@ -716,18 +793,21 @@ module ActiveSupport # define_callbacks :save, scope: [:name] # # would call <tt>Audit#save</tt>. + # + # NOTE: +method_name+ passed to `define_model_callbacks` must not end with + # `!`, `?` or `=`. 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) + + module_eval <<-RUBY, __FILE__, __LINE__ + 1 + def _run_#{name}_callbacks(&block) + _run_callbacks(_#{name}_callbacks, &block) + end + RUBY end end diff --git a/activesupport/lib/active_support/concern.rb b/activesupport/lib/active_support/concern.rb index b796d01dfd..342d3a9d52 100644 --- a/activesupport/lib/active_support/concern.rb +++ b/activesupport/lib/active_support/concern.rb @@ -26,7 +26,7 @@ module ActiveSupport # scope :disabled, -> { where(disabled: true) } # end # - # module ClassMethods + # class_methods do # ... # end # end @@ -95,7 +95,7 @@ module ActiveSupport # end # # class Host - # include Bar # works, Bar takes care now of its dependencies + # include Bar # It works, now Bar takes care of its dependencies # end module Concern class MultipleIncludedBlocks < StandardError #:nodoc: @@ -130,5 +130,13 @@ module ActiveSupport super end end + + def class_methods(&class_methods_module_definition) + mod = const_defined?(:ClassMethods) ? + const_get(:ClassMethods) : + const_set(:ClassMethods, Module.new) + + mod.module_eval(&class_methods_module_definition) + end end end diff --git a/activesupport/lib/active_support/core_ext.rb b/activesupport/lib/active_support/core_ext.rb index 199aa91020..52706c3d7a 100644 --- a/activesupport/lib/active_support/core_ext.rb +++ b/activesupport/lib/active_support/core_ext.rb @@ -1,3 +1,4 @@ -Dir["#{File.dirname(__FILE__)}/core_ext/*.rb"].each do |path| +DEPRECATED_FILES = ["#{File.dirname(__FILE__)}/core_ext/struct.rb"] +(Dir["#{File.dirname(__FILE__)}/core_ext/*.rb"] - DEPRECATED_FILES).each do |path| require path end diff --git a/activesupport/lib/active_support/core_ext/array/access.rb b/activesupport/lib/active_support/core_ext/array/access.rb index 67f58bc0fe..45b89d2705 100644 --- a/activesupport/lib/active_support/core_ext/array/access.rb +++ b/activesupport/lib/active_support/core_ext/array/access.rb @@ -5,6 +5,8 @@ class Array # %w( a b c d ).from(2) # => ["c", "d"] # %w( a b c d ).from(10) # => [] # %w().from(0) # => [] + # %w( a b c d ).from(-2) # => ["c", "d"] + # %w( a b c ).from(-10) # => [] def from(position) self[position, length] || [] end @@ -15,8 +17,14 @@ class Array # %w( a b c d ).to(2) # => ["a", "b", "c"] # %w( a b c d ).to(10) # => ["a", "b", "c", "d"] # %w().to(0) # => [] + # %w( a b c d ).to(-2) # => ["a", "b", "c"] + # %w( a b c ).to(-10) # => [] def to(position) - first position + 1 + if position >= 0 + first position + 1 + else + self[0..position] + end end # Equal to <tt>self[1]</tt>. diff --git a/activesupport/lib/active_support/core_ext/array/conversions.rb b/activesupport/lib/active_support/core_ext/array/conversions.rb index 76ffd23ed1..080e3b5ef7 100644 --- a/activesupport/lib/active_support/core_ext/array/conversions.rb +++ b/activesupport/lib/active_support/core_ext/array/conversions.rb @@ -92,7 +92,7 @@ class Array if empty? 'null' else - collect { |element| element.id }.join(',') + collect(&:id).join(',') end else to_default_s diff --git a/activesupport/lib/active_support/core_ext/array/grouping.rb b/activesupport/lib/active_support/core_ext/array/grouping.rb index 3529d57174..87ae052eb0 100644 --- a/activesupport/lib/active_support/core_ext/array/grouping.rb +++ b/activesupport/lib/active_support/core_ext/array/grouping.rb @@ -18,6 +18,11 @@ class Array # ["3", "4"] # ["5"] def in_groups_of(number, fill_with = nil) + if number.to_i <= 0 + raise ArgumentError, + "Group size must be a positive integer, was #{number.inspect}" + end + if fill_with == false collection = self else 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 39b8cea807..234283e792 100644 --- a/activesupport/lib/active_support/core_ext/big_decimal/conversions.rb +++ b/activesupport/lib/active_support/core_ext/big_decimal/conversions.rb @@ -1,31 +1,15 @@ require 'bigdecimal' require 'bigdecimal/util' -require 'yaml' class BigDecimal - YAML_MAPPING = { 'Infinity' => '.Inf', '-Infinity' => '-.Inf', 'NaN' => '.NaN' } - - def encode_with(coder) - string = to_s - coder.represent_scalar(nil, YAML_MAPPING[string] || string) - end - - # Backport this method if it doesn't exist - unless method_defined?(:to_d) - def to_d - self - end - end - DEFAULT_STRING_FORMAT = 'F' - def to_formatted_s(*args) - if args[0].is_a?(Symbol) - super + alias_method :to_default_s, :to_s + + def to_s(format = nil, options = nil) + if format.is_a?(Symbol) + to_formatted_s(format, options || {}) else - format = args[0] || DEFAULT_STRING_FORMAT - _original_to_s(format) + to_default_s(format || DEFAULT_STRING_FORMAT) end end - alias_method :_original_to_s, :to_s - alias_method :to_s, :to_formatted_s end diff --git a/activesupport/lib/active_support/core_ext/class/attribute.rb b/activesupport/lib/active_support/core_ext/class/attribute.rb index f2a221c396..f2b7bb3ef1 100644 --- a/activesupport/lib/active_support/core_ext/class/attribute.rb +++ b/activesupport/lib/active_support/core_ext/class/attribute.rb @@ -116,12 +116,4 @@ class Class attr_writer name if instance_writer end end - - private - - 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 083b165dce..84d5e95e7a 100644 --- a/activesupport/lib/active_support/core_ext/class/attribute_accessors.rb +++ b/activesupport/lib/active_support/core_ext/class/attribute_accessors.rb @@ -1,6 +1,4 @@ -require 'active_support/deprecation' +# cattr_* became mattr_* aliases in 7dfbd91b0780fbd6a1dd9bfbc176e10894871d2d, +# but we keep this around for libraries that directly require it knowing they +# want cattr_*. No need to deprecate. require 'active_support/core_ext/module/attribute_accessors' - -ActiveSupport::Deprecation.warn( - "The cattr_* method definitions have been moved into active_support/core_ext/module/attribute_accessors. Please require that instead." -) diff --git a/activesupport/lib/active_support/core_ext/class/delegating_attributes.rb b/activesupport/lib/active_support/core_ext/class/delegating_attributes.rb index c2219beb5a..1c305c5970 100644 --- a/activesupport/lib/active_support/core_ext/class/delegating_attributes.rb +++ b/activesupport/lib/active_support/core_ext/class/delegating_attributes.rb @@ -1,5 +1,7 @@ require 'active_support/core_ext/kernel/singleton_class' require 'active_support/core_ext/module/remove_method' +require 'active_support/core_ext/module/deprecation' + class Class def superclass_delegating_accessor(name, options = {}) @@ -21,6 +23,8 @@ class Class end end + deprecate superclass_delegating_accessor: :class_attribute + private # Take the object being set and store it in a method. This gives us automatic # inheritance behavior, without having to store the object in an instance 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 b85e49aca5..9525c10112 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 @@ -9,15 +9,26 @@ module DateAndTime :saturday => 5, :sunday => 6 } + WEEKEND_DAYS = [ 6, 0 ] # Returns a new date/time representing yesterday. def yesterday - advance(:days => -1) + advance(days: -1) + end + + # Returns a new date/time representing the previous day. + def prev_day + advance(days: -1) end # Returns a new date/time representing tomorrow. def tomorrow - advance(:days => 1) + advance(days: 1) + end + + # Returns a new date/time representing the next day. + def next_day + advance(days: 1) end # Returns true if the date/time is today. @@ -35,6 +46,11 @@ module DateAndTime self > self.class.current end + # Returns true if the date/time falls on a Saturday or Sunday. + def on_weekend? + WEEKEND_DAYS.include?(wday) + end + # Returns a new date/time the specified number of days ago. def days_ago(days) advance(:days => -days) @@ -111,9 +127,19 @@ module DateAndTime # Returns a new date/time representing the given day in the next week. # 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))) + # when set. +DateTime+ objects have their time set to 0:00 unless +same_time+ is true. + def next_week(given_day_in_next_week = Date.beginning_of_week, same_time: false) + result = first_hour(weeks_since(1).beginning_of_week.days_since(days_span(given_day_in_next_week))) + same_time ? copy_time_to(result) : result + end + + # Returns a new date/time representing the next weekday. + def next_weekday + if next_day.on_weekend? + next_week(:monday, same_time: true) + else + next_day + end end # Short-hand for months_since(1). @@ -134,12 +160,23 @@ module DateAndTime # Returns a new date/time representing the given day in the previous 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 prev_week(start_day = Date.beginning_of_week) - first_hour(weeks_ago(1).beginning_of_week.days_since(days_span(start_day))) + # DateTime objects have their time set to 0:00 unless +same_time+ is true. + def prev_week(start_day = Date.beginning_of_week, same_time: false) + result = first_hour(weeks_ago(1).beginning_of_week.days_since(days_span(start_day))) + same_time ? copy_time_to(result) : result end alias_method :last_week, :prev_week + # Returns a new date/time representing the previous weekday. + def prev_weekday + if prev_day.on_weekend? + copy_time_to(beginning_of_week(:friday)) + else + prev_day + end + end + alias_method :last_weekday, :prev_weekday + # Short-hand for months_ago(1). def prev_month months_ago(1) @@ -235,17 +272,20 @@ module DateAndTime end private + def first_hour(date_or_time) + date_or_time.acts_like?(:time) ? date_or_time.beginning_of_day : date_or_time + end - 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(date_or_time) + date_or_time.acts_like?(:time) ? date_or_time.end_of_day : date_or_time + end - 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) + (DAYS_INTO_WEEK[day] - DAYS_INTO_WEEK[Date.beginning_of_week]) % 7 + end - def days_span(day) - (DAYS_INTO_WEEK[day] - DAYS_INTO_WEEK[Date.beginning_of_week]) % 7 - end + def copy_time_to(other) + other.change(hour: hour, min: min, sec: sec, usec: try(:usec)) + end end end 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 73ad0aa097..55ad384f4f 100644 --- a/activesupport/lib/active_support/core_ext/date_time/calculations.rb +++ b/activesupport/lib/active_support/core_ext/date_time/calculations.rb @@ -10,7 +10,11 @@ class DateTime end end - # Seconds since midnight: DateTime.now.seconds_since_midnight. + # Returns the number of seconds since 00:00:00. + # + # DateTime.new(2012, 8, 29, 0, 0, 0).seconds_since_midnight # => 0 + # DateTime.new(2012, 8, 29, 12, 34, 56).seconds_since_midnight # => 45296 + # DateTime.new(2012, 8, 29, 23, 59, 59).seconds_since_midnight # => 86399 def seconds_since_midnight sec + (min * 60) + (hour * 3600) end @@ -53,6 +57,16 @@ class DateTime # <tt>:months</tt>, <tt>:weeks</tt>, <tt>:days</tt>, <tt>:hours</tt>, # <tt>:minutes</tt>, <tt>:seconds</tt>. def advance(options) + unless options[:weeks].nil? + options[:weeks], partial_weeks = options[:weeks].divmod(1) + options[:days] = options.fetch(:days, 0) + 7 * partial_weeks + end + + unless options[:days].nil? + options[:days], partial_days = options[:days].divmod(1) + options[:hours] = options.fetch(:hours, 0) + 24 * partial_days + end + d = to_date.advance(options) datetime_advanced_by_date = change(:year => d.year, :month => d.month, :day => d.day) seconds_to_advance = \ @@ -63,7 +77,7 @@ class DateTime if seconds_to_advance.zero? datetime_advanced_by_date else - datetime_advanced_by_date.since seconds_to_advance + datetime_advanced_by_date.since(seconds_to_advance) end end @@ -151,7 +165,9 @@ class DateTime # Layers additional behavior on DateTime#<=> so that Time and # ActiveSupport::TimeWithZone instances can be compared with a DateTime. def <=>(other) - if other.respond_to? :to_datetime + if other.kind_of?(Infinity) + super + elsif other.respond_to? :to_datetime super other.to_datetime else nil 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 6ddfb72a0d..2a9c09fc29 100644 --- a/activesupport/lib/active_support/core_ext/date_time/conversions.rb +++ b/activesupport/lib/active_support/core_ext/date_time/conversions.rb @@ -71,9 +71,9 @@ class DateTime civil(year, month, day, hour, min, sec, offset) end - # Converts +self+ to a floating-point number of seconds since the Unix epoch. + # Converts +self+ to a floating-point number of seconds, including fractional microseconds, since the Unix epoch. def to_f - seconds_since_unix_epoch.to_f + seconds_since_unix_epoch.to_f + sec_fraction end # Converts +self+ to an integer number of seconds since the Unix epoch. diff --git a/activesupport/lib/active_support/core_ext/digest/uuid.rb b/activesupport/lib/active_support/core_ext/digest/uuid.rb new file mode 100644 index 0000000000..593c51bba2 --- /dev/null +++ b/activesupport/lib/active_support/core_ext/digest/uuid.rb @@ -0,0 +1,51 @@ +require 'securerandom' + +module Digest + module UUID + DNS_NAMESPACE = "k\xA7\xB8\x10\x9D\xAD\x11\xD1\x80\xB4\x00\xC0O\xD40\xC8" #:nodoc: + URL_NAMESPACE = "k\xA7\xB8\x11\x9D\xAD\x11\xD1\x80\xB4\x00\xC0O\xD40\xC8" #:nodoc: + OID_NAMESPACE = "k\xA7\xB8\x12\x9D\xAD\x11\xD1\x80\xB4\x00\xC0O\xD40\xC8" #:nodoc: + X500_NAMESPACE = "k\xA7\xB8\x14\x9D\xAD\x11\xD1\x80\xB4\x00\xC0O\xD40\xC8" #:nodoc: + + # Generates a v5 non-random UUID (Universally Unique IDentifier). + # + # Using Digest::MD5 generates version 3 UUIDs; Digest::SHA1 generates version 5 UUIDs. + # uuid_from_hash always generates the same UUID for a given name and namespace combination. + # + # See RFC 4122 for details of UUID at: http://www.ietf.org/rfc/rfc4122.txt + def self.uuid_from_hash(hash_class, uuid_namespace, name) + if hash_class == Digest::MD5 + version = 3 + elsif hash_class == Digest::SHA1 + version = 5 + else + raise ArgumentError, "Expected Digest::SHA1 or Digest::MD5, got #{hash_class.name}." + end + + hash = hash_class.new + hash.update(uuid_namespace) + hash.update(name) + + ary = hash.digest.unpack('NnnnnN') + ary[2] = (ary[2] & 0x0FFF) | (version << 12) + ary[3] = (ary[3] & 0x3FFF) | 0x8000 + + "%08x-%04x-%04x-%04x-%04x%08x" % ary + end + + # Convenience method for uuid_from_hash using Digest::MD5. + def self.uuid_v3(uuid_namespace, name) + self.uuid_from_hash(Digest::MD5, uuid_namespace, name) + end + + # Convenience method for uuid_from_hash using Digest::SHA1. + def self.uuid_v5(uuid_namespace, name) + self.uuid_from_hash(Digest::SHA1, uuid_namespace, name) + end + + # Convenience method for SecureRandom.uuid. + def self.uuid_v4 + SecureRandom.uuid + end + end +end diff --git a/activesupport/lib/active_support/core_ext/enumerable.rb b/activesupport/lib/active_support/core_ext/enumerable.rb index 4501b7ff58..1343beb87a 100644 --- a/activesupport/lib/active_support/core_ext/enumerable.rb +++ b/activesupport/lib/active_support/core_ext/enumerable.rb @@ -35,7 +35,7 @@ module Enumerable if block_given? Hash[map { |elem| [yield(elem), elem] }] else - to_enum :index_by + to_enum(:index_by) { size if respond_to?(:size) } end end diff --git a/activesupport/lib/active_support/core_ext/file/atomic.rb b/activesupport/lib/active_support/core_ext/file/atomic.rb index 0e7e3ba378..38374af388 100644 --- a/activesupport/lib/active_support/core_ext/file/atomic.rb +++ b/activesupport/lib/active_support/core_ext/file/atomic.rb @@ -40,7 +40,7 @@ class File chown(old_stat.uid, old_stat.gid, file_name) # This operation will affect filesystem ACL's chmod(old_stat.mode, file_name) - rescue Errno::EPERM + rescue Errno::EPERM, Errno::EACCES # Changing file ownership failed, moving on. end end diff --git a/activesupport/lib/active_support/core_ext/hash.rb b/activesupport/lib/active_support/core_ext/hash.rb index f68e1662f9..af4d1da0eb 100644 --- a/activesupport/lib/active_support/core_ext/hash.rb +++ b/activesupport/lib/active_support/core_ext/hash.rb @@ -6,3 +6,4 @@ require 'active_support/core_ext/hash/indifferent_access' require 'active_support/core_ext/hash/keys' require 'active_support/core_ext/hash/reverse_merge' require 'active_support/core_ext/hash/slice' +require 'active_support/core_ext/hash/transform_values' diff --git a/activesupport/lib/active_support/core_ext/hash/compact.rb b/activesupport/lib/active_support/core_ext/hash/compact.rb index 6566215a4d..5dc9a05ec7 100644 --- a/activesupport/lib/active_support/core_ext/hash/compact.rb +++ b/activesupport/lib/active_support/core_ext/hash/compact.rb @@ -1,6 +1,6 @@ class Hash # Returns a hash with non +nil+ values. - # + # # hash = { a: true, b: false, c: nil} # hash.compact # => { a: true, b: false} # hash # => { a: true, b: false, c: nil} @@ -8,9 +8,9 @@ class Hash def compact self.select { |_, value| !value.nil? } end - + # Replaces current hash with non +nil+ values. - # + # # hash = { a: true, b: false, c: nil} # hash.compact! # => { a: true, b: false} # hash # => { a: true, b: false} diff --git a/activesupport/lib/active_support/core_ext/hash/conversions.rb b/activesupport/lib/active_support/core_ext/hash/conversions.rb index 7bea461c77..2149d4439d 100644 --- a/activesupport/lib/active_support/core_ext/hash/conversions.rb +++ b/activesupport/lib/active_support/core_ext/hash/conversions.rb @@ -105,7 +105,7 @@ class Hash # hash = Hash.from_xml(xml) # # => {"hash"=>{"foo"=>1, "bar"=>2}} # - # DisallowedType is raised if the XML contains attributes with <tt>type="yaml"</tt> or + # +DisallowedType+ is raised 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 @@ -221,7 +221,7 @@ module ActiveSupport 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) + # an XML node(where type['value'] is a Hash) value['type'] && !value['type'].is_a?(::Hash) && value.size == 1 end @@ -241,4 +241,3 @@ module ActiveSupport end end - diff --git a/activesupport/lib/active_support/core_ext/hash/deep_merge.rb b/activesupport/lib/active_support/core_ext/hash/deep_merge.rb index dc86c92003..9c9faf67ea 100644 --- a/activesupport/lib/active_support/core_ext/hash/deep_merge.rb +++ b/activesupport/lib/active_support/core_ext/hash/deep_merge.rb @@ -1,27 +1,38 @@ class Hash # Returns a new hash with +self+ and +other_hash+ merged recursively. # - # h1 = { x: { y: [4, 5, 6] }, z: [7, 8, 9] } - # h2 = { x: { y: [7, 8, 9] }, z: 'xyz' } + # h1 = { a: true, b: { c: [1, 2, 3] } } + # h2 = { a: false, b: { x: [3, 4, 5] } } # - # h1.deep_merge(h2) # => {x: {y: [7, 8, 9]}, z: "xyz"} - # h2.deep_merge(h1) # => {x: {y: [4, 5, 6]}, z: [7, 8, 9]} - # h1.deep_merge(h2) { |key, old, new| Array.wrap(old) + Array.wrap(new) } - # # => {:x=>{:y=>[4, 5, 6, 7, 8, 9]}, :z=>[7, 8, 9, "xyz"]} + # h1.deep_merge(h2) # => { a: false, b: { c: [1, 2, 3], x: [3, 4, 5] } } + # + # Like with Hash#merge in the standard library, a block can be provided + # to merge values: + # + # h1 = { a: 100, b: 200, c: { c1: 100 } } + # h2 = { b: 250, c: { c1: 200 } } + # h1.deep_merge(h2) { |key, this_val, other_val| this_val + other_val } + # # => { a: 100, b: 450, c: { c1: 300 } } def deep_merge(other_hash, &block) dup.deep_merge!(other_hash, &block) end # Same as +deep_merge+, but modifies +self+. def deep_merge!(other_hash, &block) - other_hash.each_pair do |k,v| - tv = self[k] - if tv.is_a?(Hash) && v.is_a?(Hash) - self[k] = tv.deep_merge(v, &block) + other_hash.each_pair do |current_key, other_value| + this_value = self[current_key] + + self[current_key] = if this_value.is_a?(Hash) && other_value.is_a?(Hash) + this_value.deep_merge(other_value, &block) else - self[k] = block && tv ? block.call(k, tv, v) : v + if block_given? && key?(current_key) + block.call(current_key, this_value, other_value) + else + other_value + end end end + self 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 682d089881..6e397abf51 100644 --- a/activesupport/lib/active_support/core_ext/hash/except.rb +++ b/activesupport/lib/active_support/core_ext/hash/except.rb @@ -1,13 +1,19 @@ class Hash - # Returns 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: + # Returns a hash that includes everything but the given keys. + # hash = { a: true, b: false, c: nil} + # hash.except(:c) # => { a: true, b: false} + # hash # => { a: true, b: false, c: nil} # + # This is useful for limiting a set of parameters to everything but a few known toggles: # @person.update(params[:person].except(:admin)) def except(*keys) dup.except!(*keys) end # Replaces the hash without the given keys. + # hash = { a: true, b: false, c: nil} + # hash.except!(:c) # => { a: true, b: false} + # hash # => { a: true, b: false } def except!(*keys) keys.each { |key| delete(key) } self diff --git a/activesupport/lib/active_support/core_ext/hash/indifferent_access.rb b/activesupport/lib/active_support/core_ext/hash/indifferent_access.rb index 970d6faa1d..28cb3e2a3b 100644 --- a/activesupport/lib/active_support/core_ext/hash/indifferent_access.rb +++ b/activesupport/lib/active_support/core_ext/hash/indifferent_access.rb @@ -18,6 +18,6 @@ class Hash # # b = { b: 1 } # { a: b }.with_indifferent_access['a'] # calls b.nested_under_indifferent_access - # # => {"b"=>32} + # # => {"b"=>1} alias nested_under_indifferent_access with_indifferent_access end diff --git a/activesupport/lib/active_support/core_ext/hash/keys.rb b/activesupport/lib/active_support/core_ext/hash/keys.rb index 3d41aa8572..9297a59c46 100644 --- a/activesupport/lib/active_support/core_ext/hash/keys.rb +++ b/activesupport/lib/active_support/core_ext/hash/keys.rb @@ -6,7 +6,8 @@ class Hash # hash.transform_keys{ |key| key.to_s.upcase } # # => {"NAME"=>"Rob", "AGE"=>"28"} def transform_keys - result = {} + return enum_for(:transform_keys) unless block_given? + result = self.class.new each_key do |key| result[yield(key)] = self[key] end @@ -16,6 +17,7 @@ class Hash # Destructively convert all keys using the block operations. # Same as transform_keys but modifies +self+. def transform_keys! + return enum_for(:transform_keys!) unless block_given? keys.each do |key| self[yield(key)] = delete(key) end @@ -27,15 +29,15 @@ class Hash # hash = { name: 'Rob', age: '28' } # # hash.stringify_keys - # # => { "name" => "Rob", "age" => "28" } + # # => {"name"=>"Rob", "age"=>"28"} def stringify_keys - transform_keys{ |key| key.to_s } + transform_keys(&:to_s) end # Destructively convert all keys to strings. Same as # +stringify_keys+, but modifies +self+. def stringify_keys! - transform_keys!{ |key| key.to_s } + transform_keys!(&:to_s) end # Returns a new hash with all keys converted to symbols, as long as @@ -44,7 +46,7 @@ class Hash # hash = { 'name' => 'Rob', 'age' => '28' } # # hash.symbolize_keys - # # => { name: "Rob", age: "28" } + # # => {:name=>"Rob", :age=>"28"} def symbolize_keys transform_keys{ |key| key.to_sym rescue key } end @@ -57,9 +59,11 @@ class Hash end alias_method :to_options!, :symbolize_keys! - # Validate all keys in a hash match <tt>*valid_keys</tt>, raising ArgumentError - # on a mismatch. Note that keys are NOT treated indifferently, meaning if you - # use strings for keys but assert symbols as keys, this will fail. + # Validate all keys in a hash match <tt>*valid_keys</tt>, raising + # ArgumentError on a mismatch. + # + # Note that keys are treated differently than HashWithIndifferentAccess, + # meaning that string and symbol keys will not match. # # { name: 'Rob', years: '28' }.assert_valid_keys(:name, :age) # => raises "ArgumentError: Unknown key: :years. Valid keys are: :name, :age" # { name: 'Rob', age: '28' }.assert_valid_keys('name', 'age') # => raises "ArgumentError: Unknown key: :name. Valid keys are: 'name', 'age'" @@ -75,53 +79,45 @@ class Hash # Returns a new hash with all keys converted by the block operation. # This includes the keys from the root hash and from all - # nested hashes. + # nested hashes and arrays. # # hash = { person: { name: 'Rob', age: '28' } } # # hash.deep_transform_keys{ |key| key.to_s.upcase } # # => {"PERSON"=>{"NAME"=>"Rob", "AGE"=>"28"}} def deep_transform_keys(&block) - result = {} - each do |key, value| - result[yield(key)] = value.is_a?(Hash) ? value.deep_transform_keys(&block) : value - end - result + _deep_transform_keys_in_object(self, &block) end # Destructively convert all keys by using the block operation. # This includes the keys from the root hash and from all - # nested hashes. + # nested hashes and arrays. def deep_transform_keys!(&block) - keys.each do |key| - value = delete(key) - self[yield(key)] = value.is_a?(Hash) ? value.deep_transform_keys!(&block) : value - end - self + _deep_transform_keys_in_object!(self, &block) end # Returns a new hash with all keys converted to strings. # This includes the keys from the root hash and from all - # nested hashes. + # nested hashes and arrays. # # hash = { person: { name: 'Rob', age: '28' } } # # hash.deep_stringify_keys # # => {"person"=>{"name"=>"Rob", "age"=>"28"}} def deep_stringify_keys - deep_transform_keys{ |key| key.to_s } + deep_transform_keys(&:to_s) end # Destructively convert all keys to strings. # This includes the keys from the root hash and from all - # nested hashes. + # nested hashes and arrays. def deep_stringify_keys! - deep_transform_keys!{ |key| key.to_s } + deep_transform_keys!(&:to_s) end # Returns a new hash with all keys converted to symbols, as long as # they respond to +to_sym+. This includes the keys from the root hash - # and from all nested hashes. + # and from all nested hashes and arrays. # # hash = { 'person' => { 'name' => 'Rob', 'age' => '28' } } # @@ -133,8 +129,38 @@ class Hash # Destructively convert all keys to symbols, as long as they respond # to +to_sym+. This includes the keys from the root hash and from all - # nested hashes. + # nested hashes and arrays. def deep_symbolize_keys! deep_transform_keys!{ |key| key.to_sym rescue key } end + + private + # support methods for deep transforming nested hashes and arrays + def _deep_transform_keys_in_object(object, &block) + case object + when Hash + object.each_with_object({}) do |(key, value), result| + result[yield(key)] = _deep_transform_keys_in_object(value, &block) + end + when Array + object.map {|e| _deep_transform_keys_in_object(e, &block) } + else + object + end + end + + def _deep_transform_keys_in_object!(object, &block) + case object + when Hash + object.keys.each do |key| + value = object.delete(key) + object[yield(key)] = _deep_transform_keys_in_object!(value, &block) + end + object + when Array + object.map! {|e| _deep_transform_keys_in_object!(e, &block)} + else + object + end + end end diff --git a/activesupport/lib/active_support/core_ext/hash/slice.rb b/activesupport/lib/active_support/core_ext/hash/slice.rb index 8ad600b171..41b2279013 100644 --- a/activesupport/lib/active_support/core_ext/hash/slice.rb +++ b/activesupport/lib/active_support/core_ext/hash/slice.rb @@ -1,6 +1,12 @@ class Hash - # Slice a hash to include only the given keys. This is useful for - # limiting an options hash to valid keys before passing to a method: + # Slice a hash to include only the given keys. Returns a hash containing + # the given keys. + # + # { a: 1, b: 2, c: 3, d: 4 }.slice(:a, :b) + # # => {:a=>1, :b=>2} + # + # This is useful for limiting an options hash to valid keys before + # passing to a method: # # def search(criteria = {}) # criteria.assert_valid_keys(:mass, :velocity, :time) diff --git a/activesupport/lib/active_support/core_ext/hash/transform_values.rb b/activesupport/lib/active_support/core_ext/hash/transform_values.rb new file mode 100644 index 0000000000..e9bcce761f --- /dev/null +++ b/activesupport/lib/active_support/core_ext/hash/transform_values.rb @@ -0,0 +1,23 @@ +class Hash + # Returns a new hash with the results of running +block+ once for every value. + # The keys are unchanged. + # + # { a: 1, b: 2, c: 3 }.transform_values { |x| x * 2 } + # # => { a: 2, b: 4, c: 6 } + def transform_values + return enum_for(:transform_values) unless block_given? + result = self.class.new + each do |key, value| + result[key] = yield(value) + end + result + end + + # Destructive +transform_values+ + def transform_values! + return enum_for(:transform_values!) unless block_given? + each do |key, value| + self[key] = yield(value) + end + end +end diff --git a/activesupport/lib/active_support/core_ext/kernel.rb b/activesupport/lib/active_support/core_ext/kernel.rb index 0275f4c037..364ed9d65f 100644 --- a/activesupport/lib/active_support/core_ext/kernel.rb +++ b/activesupport/lib/active_support/core_ext/kernel.rb @@ -1,4 +1,4 @@ -require 'active_support/core_ext/kernel/reporting' require 'active_support/core_ext/kernel/agnostics' -require 'active_support/core_ext/kernel/debugger' +require 'active_support/core_ext/kernel/concern' +require 'active_support/core_ext/kernel/reporting' require 'active_support/core_ext/kernel/singleton_class' diff --git a/activesupport/lib/active_support/core_ext/kernel/concern.rb b/activesupport/lib/active_support/core_ext/kernel/concern.rb new file mode 100644 index 0000000000..bf72caa058 --- /dev/null +++ b/activesupport/lib/active_support/core_ext/kernel/concern.rb @@ -0,0 +1,10 @@ +require 'active_support/core_ext/module/concerning' + +module Kernel + # A shortcut to define a toplevel concern, not within a module. + # + # See Module::Concerning for more. + def concern(topic, &module_definition) + Object.concern topic, &module_definition + end +end diff --git a/activesupport/lib/active_support/core_ext/kernel/debugger.rb b/activesupport/lib/active_support/core_ext/kernel/debugger.rb index 2073cac98d..1fde3db070 100644 --- a/activesupport/lib/active_support/core_ext/kernel/debugger.rb +++ b/activesupport/lib/active_support/core_ext/kernel/debugger.rb @@ -1,10 +1,3 @@ -module Kernel - unless respond_to?(:debugger) - # Starts a debugging session if the +debugger+ gem has been loaded (call rails server --debugger to do load it). - def debugger - message = "\n***** Debugger requested, but was not available (ensure the debugger gem is listed in Gemfile/installed as gem): Start server with --debugger to enable *****\n" - defined?(Rails) ? Rails.logger.info(message) : $stderr.puts(message) - end - alias breakpoint debugger unless respond_to?(:breakpoint) - end -end +require 'active_support/deprecation' + +ActiveSupport::Deprecation.warn("This file is deprecated and will be removed in Rails 5.1 with no replacement.") diff --git a/activesupport/lib/active_support/core_ext/kernel/reporting.rb b/activesupport/lib/active_support/core_ext/kernel/reporting.rb index 3b5e205244..9189e6d977 100644 --- a/activesupport/lib/active_support/core_ext/kernel/reporting.rb +++ b/activesupport/lib/active_support/core_ext/kernel/reporting.rb @@ -1,4 +1,3 @@ -require 'rbconfig' require 'tempfile' module Kernel @@ -29,28 +28,6 @@ module Kernel $VERBOSE = old_verbose end - # For compatibility - def silence_stderr #:nodoc: - silence_stream(STDERR) { yield } - end - - # Silences any stream for the duration of the block. - # - # silence_stream(STDOUT) do - # puts 'This will never be seen' - # end - # - # puts 'But this will' - def silence_stream(stream) - old_stream = stream.dup - stream.reopen(RbConfig::CONFIG['host_os'] =~ /mswin|mingw/ ? 'NUL:' : '/dev/null') - stream.sync = true - yield - ensure - stream.reopen(old_stream) - old_stream.close - end - # Blocks and ignores any exception passed as argument if raised within the block. # # suppress(ZeroDivisionError) do @@ -63,48 +40,4 @@ module Kernel yield rescue *exception_classes end - - # Captures the given stream and returns it: - # - # stream = capture(:stdout) { puts 'notice' } - # stream # => "notice\n" - # - # stream = capture(:stderr) { warn 'error' } - # stream # => "error\n" - # - # even for subprocesses: - # - # stream = capture(:stdout) { system('echo notice') } - # stream # => "notice\n" - # - # stream = capture(:stderr) { system('echo error 1>&2') } - # stream # => "error\n" - def capture(stream) - stream = stream.to_s - captured_stream = Tempfile.new(stream) - stream_io = eval("$#{stream}") - origin_stream = stream_io.dup - stream_io.reopen(captured_stream) - - yield - - stream_io.rewind - return captured_stream.read - ensure - captured_stream.close - captured_stream.unlink - stream_io.reopen(origin_stream) - end - alias :silence :capture - - # Silences both STDOUT and STDERR, even for subprocesses. - # - # quietly { system 'bundle install' } - def quietly - silence_stream(STDOUT) do - silence_stream(STDERR) do - yield - end - end - end end diff --git a/activesupport/lib/active_support/core_ext/load_error.rb b/activesupport/lib/active_support/core_ext/load_error.rb index fe24f3716d..d9fb392752 100644 --- a/activesupport/lib/active_support/core_ext/load_error.rb +++ b/activesupport/lib/active_support/core_ext/load_error.rb @@ -1,3 +1,5 @@ +require 'active_support/deprecation/proxy_wrappers' + class LoadError REGEXPS = [ /^no such file to load -- (.+)$/i, @@ -7,6 +9,7 @@ class LoadError ] unless method_defined?(:path) + # Returns the path which was unable to be loaded. def path @path ||= begin REGEXPS.find do |regex| @@ -17,9 +20,11 @@ class LoadError end end + # Returns true if the given path name (except perhaps for the ".rb" + # extension) is the missing file which caused the exception to be raised. def is_missing?(location) location.sub(/\.rb$/, '') == path.sub(/\.rb$/, '') end end -MissingSourceFile = LoadError
\ No newline at end of file +MissingSourceFile = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('MissingSourceFile', 'LoadError') diff --git a/activesupport/lib/active_support/core_ext/module/aliasing.rb b/activesupport/lib/active_support/core_ext/module/aliasing.rb index 580cb80413..0a6fadf928 100644 --- a/activesupport/lib/active_support/core_ext/module/aliasing.rb +++ b/activesupport/lib/active_support/core_ext/module/aliasing.rb @@ -19,9 +19,9 @@ class Module # alias_method :foo_without_feature?, :foo? # alias_method :foo?, :foo_with_feature? # - # so you can safely chain foo, foo?, and foo! with the same feature. + # so you can safely chain foo, foo?, foo! and/or foo= with the same feature. def alias_method_chain(target, feature) - # Strip out punctuation on predicates or bang methods since + # Strip out punctuation on predicates, bang or writer methods since # e.g. target?_without_feature is not a valid method name. aliased_target, punctuation = target.to_s.sub(/([?!=])$/, ''), $1 yield(aliased_target, punctuation) if block_given? diff --git a/activesupport/lib/active_support/core_ext/module/attr_internal.rb b/activesupport/lib/active_support/core_ext/module/attr_internal.rb index db07d549b0..93fb598650 100644 --- a/activesupport/lib/active_support/core_ext/module/attr_internal.rb +++ b/activesupport/lib/active_support/core_ext/module/attr_internal.rb @@ -27,10 +27,8 @@ class Module def attr_internal_define(attr_name, type) internal_name = attr_internal_ivar_name(attr_name).sub(/\A@/, '') - class_eval do # class_eval is necessary on 1.9 or else the methods a made private - # use native attr_* methods as they are faster on some Ruby implementations - send("attr_#{type}", internal_name) - end + # use native attr_* methods as they are faster on some Ruby implementations + send("attr_#{type}", internal_name) attr_name, internal_name = "#{attr_name}=", "#{internal_name}=" if type == :writer alias_method attr_name, internal_name remove_method internal_name diff --git a/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb b/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb index d317df5079..d4e6b5a1ac 100644 --- a/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb +++ b/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb @@ -203,7 +203,7 @@ class Module # include HairColors # end # - # Person.class_variable_get("@@hair_colors") #=> [:brown, :black, :blonde, :red] + # Person.class_variable_get("@@hair_colors") # => [:brown, :black, :blonde, :red] def mattr_accessor(*syms, &blk) mattr_reader(*syms, &blk) mattr_writer(*syms, &blk) diff --git a/activesupport/lib/active_support/core_ext/module/concerning.rb b/activesupport/lib/active_support/core_ext/module/concerning.rb index b22dc5ff1e..07a392404e 100644 --- a/activesupport/lib/active_support/core_ext/module/concerning.rb +++ b/activesupport/lib/active_support/core_ext/module/concerning.rb @@ -63,7 +63,7 @@ class Module # # == Mix-in noise exiled to its own file: # - # Once our chunk of behavior starts pushing the scroll-to-understand it + # Once our chunk of behavior starts pushing the scroll-to-understand it's # boundary, we give in and move it to a separate file. At this size, the # overhead feels in good proportion to the size of our extraction, despite # diluting our at-a-glance sense of how things really work. diff --git a/activesupport/lib/active_support/core_ext/module/delegation.rb b/activesupport/lib/active_support/core_ext/module/delegation.rb index 58146cdf7a..24df83800b 100644 --- a/activesupport/lib/active_support/core_ext/module/delegation.rb +++ b/activesupport/lib/active_support/core_ext/module/delegation.rb @@ -1,15 +1,23 @@ +require 'set' + class Module # Error generated by +delegate+ when a method is called on +nil+ and +allow_nil+ # option is not used. class DelegationError < NoMethodError; end + RUBY_RESERVED_WORDS = Set.new( + %w(alias and BEGIN begin break case class def defined? do else elsif END + end ensure false for if in module next nil not or redo rescue retry + return self super then true undef unless until when while yield) + ).freeze + # 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 + # * <tt>:allow_nil</tt> - if set to true, prevents a +NoMethodError+ from being 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 @@ -163,45 +171,35 @@ class Module line = line.to_i to = to.to_s - to = 'self.class' if to == 'class' + to = "self.#{to}" if RUBY_RESERVED_WORDS.include?(to) methods.each do |method| # Attribute writer methods only accept one argument. Makes sure []= # methods still accept two arguments. definition = (method =~ /[^\]]=$/) ? 'arg' : '*args, &block' - # The following generated methods call the target exactly once, storing + # The following generated method calls 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 - 3) - def #{method_prefix}#{method}(#{definition}) # def customer_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 DelegationError, "#{self}##{method_prefix}#{method} delegated to #{to}.#{method}, but #{to} is nil: \#{self.inspect}") - 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 + exception = %(raise DelegationError, "#{self}##{method_prefix}#{method} delegated to #{to}.#{method}, but #{to} is nil: \#{self.inspect}") + + method_def = [ + "def #{method_prefix}#{method}(#{definition})", + " _ = #{to}", + " if !_.nil? || nil.respond_to?(:#{method})", + " _.#{method}(#{definition})", + " else", + " #{exception unless allow_nil}", + " end", + "end" + ].join ';' + + module_eval(method_def, file, line) end 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 index b1097cc83b..1fde3db070 100644 --- a/activesupport/lib/active_support/core_ext/module/method_transplanting.rb +++ b/activesupport/lib/active_support/core_ext/module/method_transplanting.rb @@ -1,11 +1,3 @@ -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 +require 'active_support/deprecation' + +ActiveSupport::Deprecation.warn("This file is deprecated and will be removed in Rails 5.1 with no replacement.") diff --git a/activesupport/lib/active_support/core_ext/module/remove_method.rb b/activesupport/lib/active_support/core_ext/module/remove_method.rb index 719071d1c2..8a2569a7d0 100644 --- a/activesupport/lib/active_support/core_ext/module/remove_method.rb +++ b/activesupport/lib/active_support/core_ext/module/remove_method.rb @@ -1,10 +1,13 @@ class Module + # Remove the named method, if it exists. def remove_possible_method(method) if method_defined?(method) || private_method_defined?(method) undef_method(method) end end + # Replace the existing method definition, if there is one, with the contents + # of the block. def redefine_method(method, &block) remove_possible_method(method) define_method(method, &block) diff --git a/activesupport/lib/active_support/core_ext/name_error.rb b/activesupport/lib/active_support/core_ext/name_error.rb index e1ebd4f91c..b82148e4e5 100644 --- a/activesupport/lib/active_support/core_ext/name_error.rb +++ b/activesupport/lib/active_support/core_ext/name_error.rb @@ -1,5 +1,12 @@ class NameError # Extract the name of the missing constant from the exception message. + # + # begin + # HelloWorld + # rescue NameError => e + # e.missing_name + # end + # # => "HelloWorld" def missing_name if /undefined local variable or method/ !~ message $1 if /((::)?([A-Z]\w*)(::[A-Z]\w*)*)$/ =~ message @@ -7,6 +14,13 @@ class NameError end # Was this exception raised because the given name was missing? + # + # begin + # HelloWorld + # rescue NameError => e + # e.missing_name?("HelloWorld") + # end + # # => true def missing_name?(name) if name.is_a? Symbol last_name = (missing_name || '').split('::').last diff --git a/activesupport/lib/active_support/core_ext/numeric/bytes.rb b/activesupport/lib/active_support/core_ext/numeric/bytes.rb index deea8e9358..dfbca32474 100644 --- a/activesupport/lib/active_support/core_ext/numeric/bytes.rb +++ b/activesupport/lib/active_support/core_ext/numeric/bytes.rb @@ -7,36 +7,56 @@ class Numeric EXABYTE = PETABYTE * 1024 # Enables the use of byte calculations and declarations, like 45.bytes + 2.6.megabytes + # + # 2.bytes # => 2 def bytes self end alias :byte :bytes + # Returns the number of bytes equivalent to the kilobytes provided. + # + # 2.kilobytes # => 2048 def kilobytes self * KILOBYTE end alias :kilobyte :kilobytes + # Returns the number of bytes equivalent to the megabytes provided. + # + # 2.megabytes # => 2_097_152 def megabytes self * MEGABYTE end alias :megabyte :megabytes + # Returns the number of bytes equivalent to the gigabytes provided. + # + # 2.gigabytes # => 2_147_483_648 def gigabytes self * GIGABYTE end alias :gigabyte :gigabytes + # Returns the number of bytes equivalent to the terabytes provided. + # + # 2.terabytes # => 2_199_023_255_552 def terabytes self * TERABYTE end alias :terabyte :terabytes + # Returns the number of bytes equivalent to the petabytes provided. + # + # 2.petabytes # => 2_251_799_813_685_248 def petabytes self * PETABYTE end alias :petabyte :petabytes + # Returns the number of bytes equivalent to the exabytes provided. + # + # 2.exabytes # => 2_305_843_009_213_693_952 def exabytes self * EXABYTE end diff --git a/activesupport/lib/active_support/core_ext/numeric/conversions.rb b/activesupport/lib/active_support/core_ext/numeric/conversions.rb index 6d3635c69a..0c8ff79237 100644 --- a/activesupport/lib/active_support/core_ext/numeric/conversions.rb +++ b/activesupport/lib/active_support/core_ext/numeric/conversions.rb @@ -118,18 +118,28 @@ class Numeric end end - [Float, Fixnum, Bignum, BigDecimal].each do |klass| - klass.send(:alias_method, :to_default_s, :to_s) - - klass.send(:define_method, :to_s) do |*args| - if args[0].is_a?(Symbol) - format = args[0] - options = args[1] || {} + [Fixnum, Bignum].each do |klass| + klass.class_eval do + alias_method :to_default_s, :to_s + def to_s(base_or_format = 10, options = nil) + if base_or_format.is_a?(Symbol) + to_formatted_s(base_or_format, options || {}) + else + to_default_s(base_or_format) + end + end + end + end - self.to_formatted_s(format, options) + Float.class_eval do + alias_method :to_default_s, :to_s + def to_s(*args) + if args.empty? + to_default_s else - to_default_s(*args) + to_formatted_s(*args) end end end + end diff --git a/activesupport/lib/active_support/core_ext/numeric/time.rb b/activesupport/lib/active_support/core_ext/numeric/time.rb index 704c4248d9..ef32817f55 100644 --- a/activesupport/lib/active_support/core_ext/numeric/time.rb +++ b/activesupport/lib/active_support/core_ext/numeric/time.rb @@ -36,51 +36,51 @@ class Numeric end alias :second :seconds + # Returns a Duration instance matching the number of minutes provided. + # + # 2.minutes # => 120 seconds def minutes ActiveSupport::Duration.new(self * 60, [[:seconds, self * 60]]) end alias :minute :minutes + # Returns a Duration instance matching the number of hours provided. + # + # 2.hours # => 7_200 seconds def hours ActiveSupport::Duration.new(self * 3600, [[:seconds, self * 3600]]) end alias :hour :hours + # Returns a Duration instance matching the number of days provided. + # + # 2.days # => 2 days def days ActiveSupport::Duration.new(self * 24.hours, [[:days, self]]) end alias :day :days + # Returns a Duration instance matching the number of weeks provided. + # + # 2.weeks # => 14 days def weeks ActiveSupport::Duration.new(self * 7.days, [[:days, self * 7]]) end alias :week :weeks + # Returns a Duration instance matching the number of fortnights provided. + # + # 2.fortnights # => 28 days def fortnights ActiveSupport::Duration.new(self * 2.weeks, [[:days, self * 14]]) end alias :fortnight :fortnights - # Reads best without arguments: 10.minutes.ago - def ago(time = ::Time.current) - ActiveSupport::Deprecation.warn "Calling #ago or #until on a number (e.g. 5.ago) is deprecated and will be removed in the future, use 5.seconds.ago instead" - time - self - end - - # Reads best with argument: 10.minutes.until(time) - alias :until :ago - - # Reads best with argument: 10.minutes.since(time) - def since(time = ::Time.current) - ActiveSupport::Deprecation.warn "Calling #since or #from_now on a number (e.g. 5.since) is deprecated and will be removed in the future, use 5.seconds.since instead" - time + self - end - - # Reads best without arguments: 10.minutes.from_now - alias :from_now :since - - # Used with the standard time durations, like 1.hour.in_milliseconds -- + # Returns the number of milliseconds equivalent to the seconds provided. + # Used with the standard time durations, like 1.hour.in_milliseconds -- # so we can feed them to JavaScript functions like getTime(). + # + # 2.in_milliseconds # => 2_000 def in_milliseconds self * 1000 end diff --git a/activesupport/lib/active_support/core_ext/object/deep_dup.rb b/activesupport/lib/active_support/core_ext/object/deep_dup.rb index 2e99f4a1b8..0191d2e973 100644 --- a/activesupport/lib/active_support/core_ext/object/deep_dup.rb +++ b/activesupport/lib/active_support/core_ext/object/deep_dup.rb @@ -25,7 +25,7 @@ class Array # array[1][2] # => nil # dup[1][2] # => 4 def deep_dup - map { |it| it.deep_dup } + map(&:deep_dup) end end diff --git a/activesupport/lib/active_support/core_ext/object/duplicable.rb b/activesupport/lib/active_support/core_ext/object/duplicable.rb index 9cd7485e2e..620f7b6561 100644 --- a/activesupport/lib/active_support/core_ext/object/duplicable.rb +++ b/activesupport/lib/active_support/core_ext/object/duplicable.rb @@ -19,7 +19,7 @@ class Object # Can you safely dup this object? # - # False for +nil+, +false+, +true+, symbol, and number objects; + # False for +nil+, +false+, +true+, symbol, number objects; # true otherwise. def duplicable? true @@ -78,13 +78,17 @@ end require 'bigdecimal' class BigDecimal - begin - BigDecimal.new('4.56').dup + def duplicable? + true + end +end - def duplicable? - true - end - rescue TypeError - # can't dup, so use superclass implementation +class Method + # Methods are not duplicable: + # + # method(:puts).duplicable? # => false + # method(:puts).dup # => TypeError: allocator undefined for Method + def duplicable? + false end end diff --git a/activesupport/lib/active_support/core_ext/object/inclusion.rb b/activesupport/lib/active_support/core_ext/object/inclusion.rb index b5671f66d0..55f281b213 100644 --- a/activesupport/lib/active_support/core_ext/object/inclusion.rb +++ b/activesupport/lib/active_support/core_ext/object/inclusion.rb @@ -12,4 +12,16 @@ class Object rescue NoMethodError raise ArgumentError.new("The parameter passed to #in? must respond to #include?") end + + # Returns the receiver if it's included in the argument otherwise returns +nil+. + # Argument must be any object which responds to +#include?+. Usage: + # + # params[:bucket_type].presence_in %w( project calendar ) + # + # This will throw an ArgumentError if the argument doesn't respond to +#include?+. + # + # @return [Object] + def presence_in(another_object) + self.in?(another_object) ? self : nil + 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 755e1c6b16..593a7a4940 100644 --- a/activesupport/lib/active_support/core_ext/object/instance_variables.rb +++ b/activesupport/lib/active_support/core_ext/object/instance_variables.rb @@ -23,6 +23,6 @@ class Object # # C.new(0, 1).instance_variable_names # => ["@y", "@x"] def instance_variable_names - instance_variables.map { |var| var.to_s } + instance_variables.map(&:to_s) end end diff --git a/activesupport/lib/active_support/core_ext/object/json.rb b/activesupport/lib/active_support/core_ext/object/json.rb index 1675145ffe..698b2d1920 100644 --- a/activesupport/lib/active_support/core_ext/object/json.rb +++ b/activesupport/lib/active_support/core_ext/object/json.rb @@ -16,17 +16,17 @@ require 'active_support/core_ext/module/aliasing' # otherwise they will always use to_json gem implementation, which is backwards incompatible in # several cases (for instance, the JSON implementation for Hash does not work) with inheritance # and consequently classes as ActiveSupport::OrderedHash cannot be serialized to json. -# +# # On the other hand, we should avoid conflict with ::JSON.{generate,dump}(obj). Unfortunately, the # JSON gem's encoder relies on its own to_json implementation to encode objects. Since it always # passes a ::JSON::State object as the only argument to to_json, we can detect that and forward the # calls to the original to_json method. -# +# # It should be noted that when using ::JSON.{generate,dump} directly, ActiveSupport's encoder is # bypassed completely. This means that as_json won't be invoked and the JSON gem will simply # ignore any options it does not natively understand. This also means that ::JSON.{generate,dump} # should give exactly the same results with or without active support. -[Object, Array, FalseClass, Float, Hash, Integer, NilClass, String, TrueClass].each do |klass| +[Object, Array, FalseClass, Float, Hash, Integer, NilClass, String, TrueClass, Enumerable].each do |klass| klass.class_eval do def to_json_with_active_support_encoder(options = nil) if options.is_a?(::JSON::State) @@ -162,8 +162,8 @@ end class Time def as_json(options = nil) #:nodoc: - if ActiveSupport.use_standard_json_time_format - xmlschema(3) + if ActiveSupport::JSON::Encoding.use_standard_json_time_format + xmlschema(ActiveSupport::JSON::Encoding.time_precision) else %(#{strftime("%Y/%m/%d %H:%M:%S")} #{formatted_offset(false)}) end @@ -172,7 +172,7 @@ end class Date def as_json(options = nil) #:nodoc: - if ActiveSupport.use_standard_json_time_format + if ActiveSupport::JSON::Encoding.use_standard_json_time_format strftime("%Y-%m-%d") else strftime("%Y/%m/%d") @@ -182,8 +182,8 @@ end class DateTime def as_json(options = nil) #:nodoc: - if ActiveSupport.use_standard_json_time_format - xmlschema(3) + if ActiveSupport::JSON::Encoding.use_standard_json_time_format + xmlschema(ActiveSupport::JSON::Encoding.time_precision) else strftime('%Y/%m/%d %H:%M:%S %z') end diff --git a/activesupport/lib/active_support/core_ext/object/to_json.rb b/activesupport/lib/active_support/core_ext/object/to_json.rb deleted file mode 100644 index 3dcae6fc7f..0000000000 --- a/activesupport/lib/active_support/core_ext/object/to_json.rb +++ /dev/null @@ -1,5 +0,0 @@ -ActiveSupport::Deprecation.warn 'You have required `active_support/core_ext/object/to_json`. ' \ - 'This file will be removed in Rails 4.2. You should require `active_support/core_ext/object/json` ' \ - 'instead.' - -require 'active_support/core_ext/object/json' 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 3b137ce6ae..684d4ef57e 100644 --- a/activesupport/lib/active_support/core_ext/object/to_param.rb +++ b/activesupport/lib/active_support/core_ext/object/to_param.rb @@ -1,58 +1 @@ -class Object - # Alias of <tt>to_s</tt>. - def to_param - to_s - end -end - -class NilClass - # Returns +self+. - def to_param - self - end -end - -class TrueClass - # Returns +self+. - def to_param - self - end -end - -class FalseClass - # Returns +self+. - def to_param - self - end -end - -class Array - # Calls <tt>to_param</tt> on all its elements and joins the result with - # slashes. This is used by <tt>url_for</tt> in Action Pack. - def to_param - collect { |e| e.to_param }.join '/' - end -end - -class Hash - # Returns a string representation of the receiver suitable for use as a URL - # query string: - # - # {name: 'David', nationality: 'Danish'}.to_param - # # => "name=David&nationality=Danish" - # - # An optional namespace can be passed to enclose the param names: - # - # {name: 'David', nationality: 'Danish'}.to_param('user') - # # => "user[name]=David&user[nationality]=Danish" - # - # The string pairs "key=value" that conform the query string - # are sorted lexicographically in ascending order. - # - # This method is also aliased as +to_query+. - def to_param(namespace = nil) - collect do |key, value| - value.to_query(namespace ? "#{namespace}[#{key}]" : key) - end.sort! * '&' - end -end +require 'active_support/core_ext/object/to_query' diff --git a/activesupport/lib/active_support/core_ext/object/to_query.rb b/activesupport/lib/active_support/core_ext/object/to_query.rb index 5d5fcf00e0..ec5ace4e16 100644 --- a/activesupport/lib/active_support/core_ext/object/to_query.rb +++ b/activesupport/lib/active_support/core_ext/object/to_query.rb @@ -1,27 +1,84 @@ -require 'active_support/core_ext/object/to_param' +require 'cgi' class Object - # Converts an object into a string suitable for use as a URL query string, using the given <tt>key</tt> as the - # param name. - # - # Note: This method is defined as a default implementation for all Objects for Hash#to_query to work. + # Alias of <tt>to_s</tt>. + def to_param + to_s + end + + # Converts an object into a string suitable for use as a URL query string, + # using the given <tt>key</tt> as the param name. def to_query(key) - require 'cgi' unless defined?(CGI) && defined?(CGI::escape) "#{CGI.escape(key.to_param)}=#{CGI.escape(to_param.to_s)}" end end +class NilClass + # Returns +self+. + def to_param + self + end +end + +class TrueClass + # Returns +self+. + def to_param + self + end +end + +class FalseClass + # Returns +self+. + def to_param + self + end +end + class Array + # Calls <tt>to_param</tt> on all its elements and joins the result with + # slashes. This is used by <tt>url_for</tt> in Action Pack. + def to_param + collect(&:to_param).join '/' + end + # Converts an array into a string suitable for use as a URL query string, # using the given +key+ as the param name. # # ['Rails', 'coding'].to_query('hobbies') # => "hobbies%5B%5D=Rails&hobbies%5B%5D=coding" def to_query(key) prefix = "#{key}[]" - collect { |value| value.to_query(prefix) }.join '&' + + if empty? + nil.to_query(prefix) + else + collect { |value| value.to_query(prefix) }.join '&' + end end end class Hash - alias_method :to_query, :to_param + # Returns a string representation of the receiver suitable for use as a URL + # query string: + # + # {name: 'David', nationality: 'Danish'}.to_query + # # => "name=David&nationality=Danish" + # + # An optional namespace can be passed to enclose key names: + # + # {name: 'David', nationality: 'Danish'}.to_query('user') + # # => "user%5Bname%5D=David&user%5Bnationality%5D=Danish" + # + # The string pairs "key=value" that conform the query string + # are sorted lexicographically in ascending order. + # + # This method is also aliased as +to_param+. + def to_query(namespace = nil) + collect do |key, value| + unless (value.is_a?(Hash) || value.is_a?(Array)) && value.empty? + value.to_query(namespace ? "#{namespace}[#{key}]" : key) + end + end.compact.sort! * '&' + end + + alias_method :to_param, :to_query end diff --git a/activesupport/lib/active_support/core_ext/object/try.rb b/activesupport/lib/active_support/core_ext/object/try.rb index 48190e1e66..e0f70b9caa 100644 --- a/activesupport/lib/active_support/core_ext/object/try.rb +++ b/activesupport/lib/active_support/core_ext/object/try.rb @@ -9,7 +9,23 @@ class Object # # instead of # - # @person ? @person.name : nil + # @person.name if @person + # + # +try+ calls can be chained: + # + # @person.try(:spouse).try(:name) + # + # instead of + # + # @person.spouse.name if @person && @person.spouse + # + # +try+ will also return +nil+ if the receiver does not respond to the method: + # + # @person.try(:non_existing_method) # => nil + # + # instead of + # + # @person.non_existing_method if @person.respond_to?(:non_existing_method) # => nil # # +try+ returns +nil+ when called on +nil+ regardless of whether it responds # to the method: @@ -24,7 +40,7 @@ class Object # # 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. + # in case of argument mismatch. # # If +try+ is called without arguments it yields the receiver to a given # block unless it is +nil+: @@ -33,24 +49,33 @@ class Object # ... # end # - # Please also note that +try+ is defined on +Object+, therefore it won't work + # You can also call try with a block without accepting an argument, and the block + # will be instance_eval'ed instead: + # + # @person.try { upcase.truncate(50) } + # + # 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. + # the delegator itself. def try(*a, &b) - if a.empty? && block_given? - yield self - else - public_send(*a, &b) if respond_to?(a.first) - end + try!(*a, &b) if a.empty? || respond_to?(a.first) end - # Same as #try, but will raise a NoMethodError exception if the receiving is not nil and - # does not implement the tried method. + # Same as #try, but raises a NoMethodError exception if the receiver is + # not +nil+ and does not implement the tried method. + # + # "a".try!(:upcase) # => "A" + # nil.try!(:upcase) # => nil + # 123.try!(:upcase) # => NoMethodError: undefined method `upcase' for 123:Fixnum def try!(*a, &b) if a.empty? && block_given? - yield self + if b.arity.zero? + instance_eval(&b) + else + yield self + end else public_send(*a, &b) end @@ -59,12 +84,12 @@ end class NilClass # Calling +try+ on +nil+ always returns +nil+. - # It becomes specially helpful when navigating through associations that may return +nil+. + # It becomes especially helpful when navigating through associations that may return +nil+. # # nil.try(:name) # => nil # # Without +try+ - # @person && !@person.children.blank? && @person.children.first.name + # @person && @person.children.any? && @person.children.first.name # # With +try+ # @person.try(:children).try(:first).try(:name) @@ -72,6 +97,9 @@ class NilClass nil end + # Calling +try!+ on +nil+ always returns +nil+. + # + # nil.try!(:name) # => nil def try!(*args) nil end diff --git a/activesupport/lib/active_support/core_ext/object/with_options.rb b/activesupport/lib/active_support/core_ext/object/with_options.rb index 42e388b065..7d38e1d134 100644 --- a/activesupport/lib/active_support/core_ext/object/with_options.rb +++ b/activesupport/lib/active_support/core_ext/object/with_options.rb @@ -34,9 +34,36 @@ class Object # body i18n.t :body, user_name: user.name # end # + # When you don't pass an explicit receiver, it executes the whole block + # in merging options context: + # + # class Account < ActiveRecord::Base + # with_options dependent: :destroy do + # has_many :customers + # has_many :products + # has_many :invoices + # has_many :expenses + # end + # end + # # <tt>with_options</tt> can also be nested since the call is forwarded to its receiver. - # Each nesting level will merge inherited defaults in addition to their own. - def with_options(options) - yield ActiveSupport::OptionMerger.new(self, options) + # + # NOTE: Each nesting level will merge inherited defaults in addition to their own. + # + # class Post < ActiveRecord::Base + # with_options if: :persisted?, length: { minimum: 50 } do + # validates :content, if: -> { content.present? } + # end + # end + # + # The code is equivalent to: + # + # validates :content, length: { minimum: 50 }, if: -> { content.present? } + # + # Hence the inherited default for `if` key is ignored. + # + def with_options(options, &block) + option_merger = ActiveSupport::OptionMerger.new(self, options) + block.arity.zero? ? option_merger.instance_eval(&block) : block.call(option_merger) end end diff --git a/activesupport/lib/active_support/core_ext/securerandom.rb b/activesupport/lib/active_support/core_ext/securerandom.rb new file mode 100644 index 0000000000..6cdbea1f37 --- /dev/null +++ b/activesupport/lib/active_support/core_ext/securerandom.rb @@ -0,0 +1,23 @@ +require 'securerandom' + +module SecureRandom + BASE58_ALPHABET = ('0'..'9').to_a + ('A'..'Z').to_a + ('a'..'z').to_a - ['0', 'O', 'I', 'l'] + # SecureRandom.base58 generates a random base58 string. + # + # The argument _n_ specifies the length, of the random string to be generated. + # + # If _n_ is not specified or is nil, 16 is assumed. It may be larger in the future. + # + # The result may contain alphanumeric characters except 0, O, I and l + # + # p SecureRandom.base58 #=> "4kUgL2pdQMSCQtjE" + # p SecureRandom.base58(24) #=> "77TMHrHJFvFDwodq8w7Ev2m7" + # + def self.base58(n = 16) + SecureRandom.random_bytes(n).unpack("C*").map do |byte| + idx = byte % 64 + idx = SecureRandom.random_number(58) if idx >= 58 + BASE58_ALPHABET[idx] + end.join + end +end diff --git a/activesupport/lib/active_support/core_ext/string/access.rb b/activesupport/lib/active_support/core_ext/string/access.rb index 6018fd9641..ebd0dd3fc7 100644 --- a/activesupport/lib/active_support/core_ext/string/access.rb +++ b/activesupport/lib/active_support/core_ext/string/access.rb @@ -64,7 +64,7 @@ class String # Returns the first character. If a limit is supplied, returns a substring # from the beginning of the string until it reaches the limit value. If the - # given limit is greater than or equal to the string length, returns self. + # given limit is greater than or equal to the string length, returns a copy of self. # # str = "hello" # str.first # => "h" @@ -76,7 +76,7 @@ class String if limit == 0 '' elsif limit >= size - self + self.dup else to(limit - 1) end @@ -84,7 +84,7 @@ class String # Returns the last character of the string. If a limit is supplied, returns a substring # from the end of the string until it reaches the limit value (counting backwards). If - # the given limit is greater than or equal to the string length, returns self. + # the given limit is greater than or equal to the string length, returns a copy of self. # # str = "hello" # str.last # => "o" @@ -96,7 +96,7 @@ class String if limit == 0 '' elsif limit >= size - self + self.dup else from(-limit) end diff --git a/activesupport/lib/active_support/core_ext/string/filters.rb b/activesupport/lib/active_support/core_ext/string/filters.rb index 49c0df6026..096292dc58 100644 --- a/activesupport/lib/active_support/core_ext/string/filters.rb +++ b/activesupport/lib/active_support/core_ext/string/filters.rb @@ -3,7 +3,7 @@ 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). + # Note that it handles both ASCII and Unicode whitespace. # # %{ Multi-line # string }.squish # => "Multi-line string" @@ -13,6 +13,9 @@ class String end # Performs a destructive squish. See String#squish. + # str = " foo bar \n \t boo" + # str.squish! # => "foo bar boo" + # str # => "foo bar boo" def squish! gsub!(/\A[[:space:]]+/, '') gsub!(/[[:space:]]+\z/, '') @@ -20,14 +23,24 @@ class String self end - # Returns a new string with all occurrences of the pattern removed. Short-hand for String#gsub(pattern, ''). - def remove(pattern) - gsub pattern, '' + # Returns a new string with all occurrences of the patterns removed. + # str = "foo bar test" + # str.remove(" test") # => "foo bar" + # str # => "foo bar test" + def remove(*patterns) + dup.remove!(*patterns) end - # Alters the string by removing all occurrences of the pattern. Short-hand for String#gsub!(pattern, ''). - def remove!(pattern) - gsub! pattern, '' + # Alters the string by removing all occurrences of the patterns. + # str = "foo bar test" + # str.remove!(" test") # => "foo bar" + # str # => "foo bar" + def remove!(*patterns) + patterns.each do |pattern| + gsub! pattern, "" + end + + self end # Truncates a given +text+ after a given <tt>length</tt> if +text+ is longer than <tt>length</tt>: @@ -62,4 +75,28 @@ class String "#{self[0, stop]}#{omission}" end + + # Truncates a given +text+ after a given number of words (<tt>words_count</tt>): + # + # 'Once upon a time in a world far far away'.truncate_words(4) + # # => "Once upon a time..." + # + # Pass a string or regexp <tt>:separator</tt> to specify a different separator of words: + # + # 'Once<br>upon<br>a<br>time<br>in<br>a<br>world'.truncate_words(5, separator: '<br>') + # # => "Once<br>upon<br>a<br>time<br>in..." + # + # The last characters will be replaced with the <tt>:omission</tt> string (defaults to "..."): + # + # 'And they found that many people were sleeping better.'.truncate_words(5, omission: '... (continued)') + # # => "And they found that many... (continued)" + def truncate_words(words_count, options = {}) + sep = options[:separator] || /\s+/ + sep = Regexp.escape(sep.to_s) unless Regexp === sep + if self =~ /\A((?:.+?#{sep}){#{words_count - 1}}.+?)#{sep}.*/m + $1 + (options[:omission] || '...') + else + dup + end + end end diff --git a/activesupport/lib/active_support/core_ext/string/inflections.rb b/activesupport/lib/active_support/core_ext/string/inflections.rb index cf9b1a4ec0..38d567c014 100644 --- a/activesupport/lib/active_support/core_ext/string/inflections.rb +++ b/activesupport/lib/active_support/core_ext/string/inflections.rb @@ -31,7 +31,7 @@ class String def pluralize(count = nil, locale = :en) locale = count if count.is_a?(Symbol) if count == 1 - self + self.dup else ActiveSupport::Inflector.pluralize(self, locale) end @@ -130,6 +130,8 @@ class String # # 'ActiveRecord::CoreExtensions::String::Inflections'.demodulize # => "Inflections" # 'Inflections'.demodulize # => "Inflections" + # '::Inflections'.demodulize # => "Inflections" + # ''.demodulize # => '' # # See also +deconstantize+. def demodulize @@ -197,6 +199,7 @@ class String # 'employee_salary'.humanize # => "Employee salary" # 'author_id'.humanize # => "Author" # 'author_id'.humanize(capitalize: false) # => "author" + # '_id'.humanize # => "Id" def humanize(options = {}) ActiveSupport::Inflector.humanize(self, options) 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 eb02b6a442..ba8d4acd6d 100644 --- a/activesupport/lib/active_support/core_ext/string/output_safety.rb +++ b/activesupport/lib/active_support/core_ext/string/output_safety.rb @@ -6,7 +6,7 @@ class ERB HTML_ESCAPE = { '&' => '&', '>' => '>', '<' => '<', '"' => '"', "'" => ''' } JSON_ESCAPE = { '&' => '\u0026', '>' => '\u003e', '<' => '\u003c', "\u2028" => '\u2028', "\u2029" => '\u2029' } HTML_ESCAPE_REGEXP = /[&"'><]/ - HTML_ESCAPE_ONCE_REGEXP = /["><']|&(?!([a-zA-Z]+|(#\d+));)/ + HTML_ESCAPE_ONCE_REGEXP = /["><']|&(?!([a-zA-Z]+|(#\d+)|(#[xX][\dA-Fa-f]+));)/ JSON_ESCAPE_REGEXP = /[\u2028\u2029&><]/u # A utility method for escaping HTML tag characters. @@ -18,12 +18,7 @@ class ERB # puts html_escape('is a > 0 & a < 10?') # # => is a > 0 & a < 10? def html_escape(s) - s = s.to_s - if s.html_safe? - s - else - s.gsub(HTML_ESCAPE_REGEXP, HTML_ESCAPE).html_safe - end + unwrapped_html_escape(s).html_safe end # Aliasing twice issues a warning "discarding old...". Remove first to avoid it. @@ -35,6 +30,18 @@ class ERB singleton_class.send(:remove_method, :html_escape) module_function :html_escape + # HTML escapes strings but doesn't wrap them with an ActiveSupport::SafeBuffer. + # This method is not for public consumption! Seriously! + def unwrapped_html_escape(s) # :nodoc: + s = s.to_s + if s.html_safe? + s + else + s.gsub(HTML_ESCAPE_REGEXP, HTML_ESCAPE) + end + end + module_function :unwrapped_html_escape + # A utility method for escaping HTML without affecting existing escaped entities. # # html_escape_once('1 < 2 & 3') @@ -124,7 +131,7 @@ module ActiveSupport #:nodoc: class SafeBuffer < String UNSAFE_STRING_METHODS = %w( capitalize chomp chop delete downcase gsub lstrip next reverse rstrip - slice squeeze strip sub succ swapcase tr tr_s upcase prepend + slice squeeze strip sub succ swapcase tr tr_s upcase ) alias_method :original_concat, :concat @@ -142,7 +149,11 @@ module ActiveSupport #:nodoc: else if html_safe? new_safe_buffer = super - new_safe_buffer.instance_eval { @html_safe = true } + + if new_safe_buffer + new_safe_buffer.instance_variable_set :@html_safe, true + end + new_safe_buffer else to_str[*args] @@ -170,14 +181,14 @@ module ActiveSupport #:nodoc: end def concat(value) - if !html_safe? || value.html_safe? - super(value) - else - super(ERB::Util.h(value)) - end + super(html_escape_interpolated_argument(value)) end alias << concat + def prepend(value) + super(html_escape_interpolated_argument(value)) + end + def +(other) dup.concat(other) end @@ -227,12 +238,18 @@ module ActiveSupport #:nodoc: private def html_escape_interpolated_argument(arg) - (!html_safe? || arg.html_safe?) ? arg : ERB::Util.h(arg) + (!html_safe? || arg.html_safe?) ? arg : + arg.to_s.gsub(ERB::Util::HTML_ESCAPE_REGEXP, ERB::Util::HTML_ESCAPE) end end end class String + # Marks a string as trusted safe. It will be inserted into HTML with no + # additional escaping performed. It is your responsibilty to ensure that the + # string contains no malicious content. This method is equivalent to the + # `raw` helper in views. It is recommended that you use `sanitize` instead of + # this method. It should never be called on user input. def html_safe ActiveSupport::SafeBuffer.new(self) end diff --git a/activesupport/lib/active_support/core_ext/struct.rb b/activesupport/lib/active_support/core_ext/struct.rb index c2c30044f2..1fde3db070 100644 --- a/activesupport/lib/active_support/core_ext/struct.rb +++ b/activesupport/lib/active_support/core_ext/struct.rb @@ -1,6 +1,3 @@ -# Backport of Struct#to_h from Ruby 2.0 -class Struct # :nodoc: - def to_h - Hash[members.zip(values)] - end -end unless Struct.instance_methods.include?(:to_h) +require 'active_support/deprecation' + +ActiveSupport::Deprecation.warn("This file is deprecated and will be removed in Rails 5.1 with no replacement.") diff --git a/activesupport/lib/active_support/core_ext/thread.rb b/activesupport/lib/active_support/core_ext/thread.rb deleted file mode 100644 index ac1ffa4128..0000000000 --- a/activesupport/lib/active_support/core_ext/thread.rb +++ /dev/null @@ -1,79 +0,0 @@ -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 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.rb b/activesupport/lib/active_support/core_ext/time.rb index 32cffe237d..72c3234630 100644 --- a/activesupport/lib/active_support/core_ext/time.rb +++ b/activesupport/lib/active_support/core_ext/time.rb @@ -1,5 +1,4 @@ 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/marshal' require 'active_support/core_ext/time/zones' diff --git a/activesupport/lib/active_support/core_ext/time/calculations.rb b/activesupport/lib/active_support/core_ext/time/calculations.rb index 89cd7516cd..649dc52865 100644 --- a/activesupport/lib/active_support/core_ext/time/calculations.rb +++ b/activesupport/lib/active_support/core_ext/time/calculations.rb @@ -64,11 +64,12 @@ class Time # Returns a new Time where one or more of the elements have been changed according # to the +options+ parameter. The time options (<tt>:hour</tt>, <tt>:min</tt>, - # <tt>:sec</tt>, <tt>:usec</tt>) reset cascadingly, so if only the hour is passed, - # then minute, sec, and usec is set to 0. If the hour and minute is passed, then - # sec and usec is set to 0. The +options+ parameter takes a hash with any of these - # keys: <tt>:year</tt>, <tt>:month</tt>, <tt>:day</tt>, <tt>:hour</tt>, <tt>:min</tt>, - # <tt>:sec</tt>, <tt>:usec</tt>. + # <tt>:sec</tt>, <tt>:usec</tt>, <tt>:nsec</tt>) reset cascadingly, so if only + # the hour is passed, then minute, sec, usec and nsec is set to 0. If the hour + # and minute is passed, then sec, usec and nsec is set to 0. The +options+ + # parameter takes a hash with any of these keys: <tt>:year</tt>, <tt>:month</tt>, + # <tt>:day</tt>, <tt>:hour</tt>, <tt>:min</tt>, <tt>:sec</tt>, <tt>:usec</tt> + # <tt>:nsec</tt>. Path either <tt>:usec</tt> or <tt>:nsec</tt>, not both. # # Time.new(2012, 8, 29, 22, 35, 0).change(day: 1) # => Time.new(2012, 8, 1, 22, 35, 0) # Time.new(2012, 8, 29, 22, 35, 0).change(year: 1981, day: 1) # => Time.new(1981, 8, 1, 22, 35, 0) @@ -80,13 +81,20 @@ class Time new_hour = options.fetch(:hour, hour) new_min = options.fetch(:min, options[:hour] ? 0 : min) new_sec = options.fetch(:sec, (options[:hour] || options[:min]) ? 0 : sec) - new_usec = options.fetch(:usec, (options[:hour] || options[:min] || options[:sec]) ? 0 : Rational(nsec, 1000)) + + if new_nsec = options[:nsec] + raise ArgumentError, "Can't change both :nsec and :usec at the same time: #{options.inspect}" if options[:usec] + new_usec = Rational(new_nsec, 1000) + else + new_usec = options.fetch(:usec, (options[:hour] || options[:min] || options[:sec]) ? 0 : Rational(nsec, 1000)) + end if utc? ::Time.utc(new_year, new_month, new_day, new_hour, new_min, new_sec, new_usec) elsif zone ::Time.local(new_year, new_month, new_day, new_hour, new_min, new_sec, new_usec) else + raise ArgumentError, 'argument out of range' if new_usec > 999999 ::Time.new(new_year, new_month, new_day, new_hour, new_min, new_sec + (new_usec.to_r / 1000000), utc_offset) end end @@ -154,7 +162,7 @@ class Time 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) + # Returns a new Time representing the end of the day, 23:59:59.999999 def end_of_day change( :hour => 23, @@ -171,7 +179,7 @@ class Time end alias :at_beginning_of_hour :beginning_of_hour - # Returns a new Time representing the end of the hour, x:59:59.999999 (.999999999 in ruby1.9) + # Returns a new Time representing the end of the hour, x:59:59.999999 def end_of_hour change( :min => 59, @@ -187,7 +195,7 @@ class Time 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) + # Returns a new Time representing the end of the minute, x:xx:59.999999 def end_of_minute change( :sec => 59, diff --git a/activesupport/lib/active_support/core_ext/time/conversions.rb b/activesupport/lib/active_support/core_ext/time/conversions.rb index 9fd26156c7..dbf1f2f373 100644 --- a/activesupport/lib/active_support/core_ext/time/conversions.rb +++ b/activesupport/lib/active_support/core_ext/time/conversions.rb @@ -20,7 +20,7 @@ class Time :iso8601 => lambda { |time| time.iso8601 } } - # Converts to a formatted string. See DATE_FORMATS for builtin formats. + # Converts to a formatted string. See DATE_FORMATS for built-in formats. # # This method is aliased to <tt>to_s</tt>. # diff --git a/activesupport/lib/active_support/core_ext/time/marshal.rb b/activesupport/lib/active_support/core_ext/time/marshal.rb index 497c4c3fb8..467bad1726 100644 --- a/activesupport/lib/active_support/core_ext/time/marshal.rb +++ b/activesupport/lib/active_support/core_ext/time/marshal.rb @@ -1,30 +1,3 @@ -# Ruby 1.9.2 adds utc_offset and zone to Time, but marshaling only -# preserves utc_offset. Preserve zone also, even though it may not -# work in some edge cases. -if Time.local(2010).zone != Marshal.load(Marshal.dump(Time.local(2010))).zone - class Time - class << self - alias_method :_load_without_zone, :_load - def _load(marshaled_time) - time = _load_without_zone(marshaled_time) - time.instance_eval do - if zone = defined?(@_zone) && remove_instance_variable('@_zone') - ary = to_a - ary[0] += subsec if ary[0] == sec - ary[-1] = zone - utc? ? Time.utc(*ary) : Time.local(*ary) - else - self - end - end - end - end +require 'active_support/deprecation' - alias_method :_dump_without_zone, :_dump - def _dump(*args) - obj = dup - obj.instance_variable_set('@_zone', zone) - obj.send :_dump_without_zone, *args - end - end -end +ActiveSupport::Deprecation.warn("This is deprecated and will be removed in Rails 5.1 with no replacement.") diff --git a/activesupport/lib/active_support/core_ext/time/zones.rb b/activesupport/lib/active_support/core_ext/time/zones.rb index bbda04d60c..0668eadb1e 100644 --- a/activesupport/lib/active_support/core_ext/time/zones.rb +++ b/activesupport/lib/active_support/core_ext/time/zones.rb @@ -1,4 +1,5 @@ require 'active_support/time_with_zone' +require 'active_support/core_ext/time/acts_like' require 'active_support/core_ext/date_and_time/zones' class Time @@ -50,7 +51,16 @@ class Time end end - # Returns a TimeZone instance or nil, or raises an ArgumentError for invalid timezones. + # Returns a TimeZone instance matching the time zone provided. + # Accepts the time zone in any format supported by <tt>Time.zone=</tt>. + # Raises an ArgumentError for invalid time zones. + # + # Time.find_zone! "America/New_York" # => #<ActiveSupport::TimeZone @name="America/New_York" ...> + # Time.find_zone! "EST" # => #<ActiveSupport::TimeZone @name="EST" ...> + # Time.find_zone! -5.hours # => #<ActiveSupport::TimeZone @name="Bogota" ...> + # Time.find_zone! nil # => nil + # Time.find_zone! false # => false + # Time.find_zone! "NOT-A-TIMEZONE" # => ArgumentError: Invalid Timezone: NOT-A-TIMEZONE def find_zone!(time_zone) if !time_zone || time_zone.is_a?(ActiveSupport::TimeZone) time_zone @@ -71,6 +81,12 @@ class Time raise ArgumentError, "Invalid Timezone: #{time_zone}" end + # Returns a TimeZone instance matching the time zone provided. + # Accepts the time zone in any format supported by <tt>Time.zone=</tt>. + # Returns +nil+ for invalid time zones. + # + # Time.find_zone "America/New_York" # => #<ActiveSupport::TimeZone @name="America/New_York" ...> + # Time.find_zone "NOT-A-TIMEZONE" # => nil def find_zone(time_zone) find_zone!(time_zone) rescue nil end diff --git a/activesupport/lib/active_support/dependencies.rb b/activesupport/lib/active_support/dependencies.rb index 6be19771f5..e03e7c30d8 100644 --- a/activesupport/lib/active_support/dependencies.rb +++ b/activesupport/lib/active_support/dependencies.rb @@ -30,6 +30,10 @@ module ActiveSupport #:nodoc: mattr_accessor :loaded self.loaded = Set.new + # Stack of files being loaded. + mattr_accessor :loading + self.loading = [] + # Should we load files or require them? mattr_accessor :mechanism self.mechanism = ENV['NO_RELOAD'] ? :require : :load @@ -180,13 +184,14 @@ module ActiveSupport #:nodoc: Dependencies.load_missing_constant(from_mod, const_name) end - # Dependencies assumes the name of the module reflects the nesting (unless - # it can be proven that is not the case), and the path to the file that - # defines the constant. Anonymous modules cannot follow these conventions - # and we assume therefore the user wants to refer to a top-level constant. + # We assume that the name of the module reflects the nesting + # (unless it can be proven that is not the case) and the path to the file + # that defines the constant. Anonymous modules cannot follow these + # conventions and therefore we assume that the user wants to refer to a + # top-level constant. def guess_for_anonymous(const_name) if Object.const_defined?(const_name) - raise NameError, "#{const_name} cannot be autoloaded from an anonymous class or module" + raise NameError.new "#{const_name} cannot be autoloaded from an anonymous class or module", const_name else Object end @@ -200,7 +205,10 @@ module ActiveSupport #:nodoc: # Object includes this module. module Loadable #:nodoc: def self.exclude_from(base) - base.class_eval { define_method(:load, Kernel.instance_method(:load)) } + base.class_eval do + define_method(:load, Kernel.instance_method(:load)) + private :load + end end def require_or_load(file_name) @@ -236,18 +244,6 @@ module ActiveSupport #:nodoc: raise end - def load(file, wrap = false) - result = false - load_dependency(file) { result = super } - result - end - - def require(file) - result = false - load_dependency(file) { result = super } - result - end - # Mark the given constant as unloadable. Unloadable constants are removed # each time dependencies are cleared. # @@ -264,6 +260,20 @@ module ActiveSupport #:nodoc: def unloadable(const_desc) Dependencies.mark_for_unload const_desc end + + private + + def load(file, wrap = false) + result = false + load_dependency(file) { result = super } + result + end + + def require(file) + result = false + load_dependency(file) { result = super } + result + end end # Exception file-blaming. @@ -316,6 +326,7 @@ module ActiveSupport #:nodoc: def clear log_call loaded.clear + loading.clear remove_unloadable_constants! end @@ -328,6 +339,7 @@ module ActiveSupport #:nodoc: # Record that we've seen this file *before* loading it to avoid an # infinite loop with mutual dependencies. loaded << expanded + loading << expanded begin if load? @@ -350,6 +362,8 @@ module ActiveSupport #:nodoc: rescue Exception loaded.delete expanded raise + ensure + loading.pop end # Record history *after* loading so first load gets warnings. @@ -407,7 +421,8 @@ module ActiveSupport #:nodoc: end def load_once_path?(path) - # to_s works around a ruby1.9 issue where #starts_with?(Pathname) will always return false + # to_s works around a ruby issue where String#starts_with?(Pathname) + # will raise a TypeError: no implicit conversion of Pathname into String autoload_once_paths.any? { |base| path.starts_with? base.to_s } end @@ -473,7 +488,7 @@ module ActiveSupport #:nodoc: expanded = File.expand_path(file_path) expanded.sub!(/\.rb\z/, '') - if loaded.include?(expanded) + if loading.include?(expanded) raise "Circular dependency detected while autoloading constant #{qualified_name}" else require_or_load(expanded, qualified_name) @@ -514,9 +529,9 @@ module ActiveSupport #:nodoc: end end - raise NameError, - "uninitialized constant #{qualified_name}", - caller.reject { |l| l.starts_with? __FILE__ } + name_error = NameError.new("uninitialized constant #{qualified_name}", const_name) + name_error.set_backtrace(caller.reject {|l| l.starts_with? __FILE__ }) + raise name_error end # Remove the constants that have been autoloaded, and those that have been @@ -665,6 +680,14 @@ module ActiveSupport #:nodoc: constants = normalized.split('::') to_remove = constants.pop + # Remove the file path from the loaded list. + file_path = search_for_file(const.underscore) + if file_path + expanded = File.expand_path(file_path) + expanded.sub!(/\.rb\z/, '') + self.loaded.delete(expanded) + end + if constants.empty? parent = Object else @@ -719,7 +742,7 @@ module ActiveSupport #:nodoc: protected def log_call(*args) if log_activity? - arg_str = args.collect { |arg| arg.inspect } * ', ' + arg_str = args.collect(&:inspect) * ', ' /in `([a-z_\?\!]+)'/ =~ caller(1).first selector = $1 || '<unknown>' log "called #{selector}(#{arg_str})" diff --git a/activesupport/lib/active_support/dependencies/autoload.rb b/activesupport/lib/active_support/dependencies/autoload.rb index c0dba5f7fd..13036d521d 100644 --- a/activesupport/lib/active_support/dependencies/autoload.rb +++ b/activesupport/lib/active_support/dependencies/autoload.rb @@ -67,7 +67,7 @@ module ActiveSupport end def eager_load! - @_autoloads.values.each { |file| require file } + @_autoloads.each_value { |file| require file } end def autoloads diff --git a/activesupport/lib/active_support/deprecation.rb b/activesupport/lib/active_support/deprecation.rb index ab16977bda..46e9996d59 100644 --- a/activesupport/lib/active_support/deprecation.rb +++ b/activesupport/lib/active_support/deprecation.rb @@ -32,7 +32,7 @@ module ActiveSupport # and the second is a library name # # ActiveSupport::Deprecation.new('2.0', 'MyLibrary') - def initialize(deprecation_horizon = '4.2', gem_name = 'Rails') + def initialize(deprecation_horizon = '5.0', gem_name = 'Rails') self.gem_name = gem_name self.deprecation_horizon = deprecation_horizon # By default, warnings are not silenced and debugging is off. diff --git a/activesupport/lib/active_support/deprecation/behaviors.rb b/activesupport/lib/active_support/deprecation/behaviors.rb index 328b8c320a..9f9dca8453 100644 --- a/activesupport/lib/active_support/deprecation/behaviors.rb +++ b/activesupport/lib/active_support/deprecation/behaviors.rb @@ -20,7 +20,7 @@ module ActiveSupport log: ->(message, callstack) { logger = - if defined?(Rails) && Rails.logger + if defined?(Rails.logger) && Rails.logger Rails.logger else require 'active_support/logger' diff --git a/activesupport/lib/active_support/duration.rb b/activesupport/lib/active_support/duration.rb index 7df4857c25..5a64fc52cc 100644 --- a/activesupport/lib/active_support/duration.rb +++ b/activesupport/lib/active_support/duration.rb @@ -1,4 +1,3 @@ -require 'active_support/proxy_object' require 'active_support/core_ext/array/conversions' require 'active_support/core_ext/object/acts_like' @@ -7,7 +6,7 @@ module ActiveSupport # Time#advance, respectively. It mainly supports the methods on Numeric. # # 1.month.ago # equivalent to Time.now.advance(months: -1) - class Duration < ProxyObject + class Duration attr_accessor :value, :parts def initialize(value, parts) #:nodoc: @@ -39,6 +38,10 @@ module ActiveSupport end alias :kind_of? :is_a? + def instance_of?(klass) # :nodoc: + Duration == klass || value.instance_of?(klass) + end + # Returns +true+ if +other+ is also a Duration instance with the # same +value+, or if <tt>other == value</tt>. def ==(other) @@ -49,6 +52,20 @@ module ActiveSupport end end + def to_s + @value.to_s + end + + # Returns +true+ if +other+ is also a Duration instance, which has the + # same parts as this one. + def eql?(other) + Duration === other && other.value.eql?(value) + end + + def hash + @value.hash + end + def self.===(other) #:nodoc: other.is_a?(Duration) rescue ::NoMethodError @@ -74,13 +91,19 @@ module ActiveSupport 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) + to_sentence(locale: ::I18n.default_locale) end def as_json(options = nil) #:nodoc: to_i end + def respond_to_missing?(method, include_private=false) #:nodoc: + @value.respond_to?(method, include_private) + end + + delegate :<=>, to: :value + protected def sum(sign, time = ::Time.current) #:nodoc: @@ -99,14 +122,6 @@ module ActiveSupport private - # We define it as a workaround to Ruby 2.0.0-p353 bug. - # For more information, check rails/rails#13055. - # It should be dropped once a new Ruby patch-level - # release after 2.0.0-p353 happens. - def ===(other) #:nodoc: - value === other - end - def method_missing(method, *args, &block) #:nodoc: value.send(method, *args, &block) end diff --git a/activesupport/lib/active_support/file_watcher.rb b/activesupport/lib/active_support/file_watcher.rb deleted file mode 100644 index 81e63e76a7..0000000000 --- a/activesupport/lib/active_support/file_watcher.rb +++ /dev/null @@ -1,36 +0,0 @@ -module ActiveSupport - class FileWatcher - class Backend - def initialize(path, watcher) - @watcher = watcher - @path = path - end - - def trigger(files) - @watcher.trigger(files) - end - end - - def initialize - @regex_matchers = {} - end - - def watch(pattern, &block) - @regex_matchers[pattern] = block - end - - def trigger(files) - trigger_files = Hash.new { |h,k| h[k] = Hash.new { |h2,k2| h2[k2] = [] } } - - files.each do |file, state| - @regex_matchers.each do |pattern, block| - trigger_files[block][state] << file if pattern === file - end - end - - trigger_files.each do |block, payload| - block.call payload - end - end - end -end diff --git a/activesupport/lib/active_support/gem_version.rb b/activesupport/lib/active_support/gem_version.rb new file mode 100644 index 0000000000..7068f09d87 --- /dev/null +++ b/activesupport/lib/active_support/gem_version.rb @@ -0,0 +1,15 @@ +module ActiveSupport + # Returns the version of the currently loaded Active Support as a <tt>Gem::Version</tt> + def self.gem_version + Gem::Version.new VERSION::STRING + end + + module VERSION + MAJOR = 5 + MINOR = 0 + TINY = 0 + PRE = "alpha" + + STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".") + end +end diff --git a/activesupport/lib/active_support/hash_with_indifferent_access.rb b/activesupport/lib/active_support/hash_with_indifferent_access.rb index f690eab604..1468c62151 100644 --- a/activesupport/lib/active_support/hash_with_indifferent_access.rb +++ b/activesupport/lib/active_support/hash_with_indifferent_access.rb @@ -1,4 +1,5 @@ require 'active_support/core_ext/hash/keys' +require 'active_support/core_ext/hash/reverse_merge' module ActiveSupport # Implements a hash where keys <tt>:foo</tt> and <tt>"foo"</tt> are considered @@ -55,7 +56,7 @@ module ActiveSupport end def initialize(constructor = {}) - if constructor.is_a?(Hash) + if constructor.respond_to?(:to_hash) super() update(constructor) else @@ -72,8 +73,10 @@ module ActiveSupport end def self.new_from_hash_copying_default(hash) + hash = hash.to_hash new(hash).tap do |new_hash| new_hash.default = hash.default + new_hash.default_proc = hash.default_proc if hash.default_proc end end @@ -125,7 +128,7 @@ module ActiveSupport if other_hash.is_a? HashWithIndifferentAccess super(other_hash) else - other_hash.each_pair do |key, value| + other_hash.to_hash.each_pair do |key, value| if block_given? && key?(key) value = yield(convert_key(key), self[key], value) end @@ -159,7 +162,7 @@ module ActiveSupport # # counters.fetch('foo') # => 1 # counters.fetch(:bar, 0) # => 0 - # counters.fetch(:bar) {|key| 0} # => 0 + # counters.fetch(:bar) { |key| 0 } # => 0 # counters.fetch(:zoo) # => KeyError: key not found: "zoo" def fetch(key, *extras) super(convert_key(key), *extras) @@ -172,10 +175,17 @@ module ActiveSupport # hash[:b] = 'y' # hash.values_at('a', 'b') # => ["x", "y"] def values_at(*indices) - indices.collect {|key| self[convert_key(key)]} + indices.collect { |key| self[convert_key(key)] } end - # Returns an exact copy of the hash. + # Returns a shallow copy of the hash. + # + # hash = ActiveSupport::HashWithIndifferentAccess.new({ a: { b: 'b' } }) + # dup = hash.dup + # dup[:a][:c] = 'c' + # + # hash[:a][:c] # => nil + # dup[:a][:c] # => "c" def dup self.class.new(self).tap do |new_hash| new_hash.default = default @@ -228,16 +238,20 @@ module ActiveSupport def to_options!; self end def select(*args, &block) - dup.tap {|hash| hash.select!(*args, &block)} + dup.tap { |hash| hash.select!(*args, &block) } + end + + def reject(*args, &block) + dup.tap { |hash| hash.reject!(*args, &block) } end # Convert to a regular hash with string keys. def to_hash - _new_hash= {} + _new_hash = Hash.new(default) each do |key, value| - _new_hash[convert_key(key)] = convert_value(value, for: :to_hash) + _new_hash[key] = convert_value(value, for: :to_hash) end - Hash.new(default).merge!(_new_hash) + _new_hash end protected diff --git a/activesupport/lib/active_support/i18n_railtie.rb b/activesupport/lib/active_support/i18n_railtie.rb index 23cd6716e3..9e742b1917 100644 --- a/activesupport/lib/active_support/i18n_railtie.rb +++ b/activesupport/lib/active_support/i18n_railtie.rb @@ -8,8 +8,6 @@ module I18n config.i18n.railties_load_path = [] config.i18n.load_path = [] config.i18n.fallbacks = ActiveSupport::OrderedOptions.new - # Enforce I18n to check the available locales when setting a locale. - config.i18n.enforce_available_locales = true # Set the i18n configuration after initialization since a lot of # configuration is still usually done in application initializers. @@ -36,7 +34,7 @@ module I18n # Avoid issues with setting the default_locale by disabling available locales # check while configuring. enforce_available_locales = app.config.i18n.delete(:enforce_available_locales) - enforce_available_locales = I18n.enforce_available_locales unless I18n.enforce_available_locales.nil? + enforce_available_locales = I18n.enforce_available_locales if enforce_available_locales.nil? I18n.enforce_available_locales = false app.config.i18n.each do |setting, value| @@ -57,7 +55,13 @@ module I18n reloader = ActiveSupport::FileUpdateChecker.new(I18n.load_path.dup){ I18n.reload! } app.reloaders << reloader - ActionDispatch::Reloader.to_prepare { reloader.execute_if_updated } + ActionDispatch::Reloader.to_prepare do + reloader.execute_if_updated + # TODO: remove the following line as soon as the return value of + # callbacks is ignored, that is, returning `false` does not + # display a deprecation warning or halts the callback chain. + true + end reloader.execute @i18n_inited = true diff --git a/activesupport/lib/active_support/inflections.rb b/activesupport/lib/active_support/inflections.rb index 4ea6abfa12..2ca1124e76 100644 --- a/activesupport/lib/active_support/inflections.rb +++ b/activesupport/lib/active_support/inflections.rb @@ -1,5 +1,11 @@ require 'active_support/inflector/inflections' +#-- +# Defines the standard inflection rules. These are the starting point for +# new projects and are not considered complete. The current set of inflection +# rules is frozen. This means, we do not change them to become more complete. +# This is a safety measure to keep existing applications from breaking. +#++ module ActiveSupport Inflector.inflections(:en) do |inflect| inflect.plural(/$/, 's') diff --git a/activesupport/lib/active_support/inflector/inflections.rb b/activesupport/lib/active_support/inflector/inflections.rb index eda0edff28..486838bd15 100644 --- a/activesupport/lib/active_support/inflector/inflections.rb +++ b/activesupport/lib/active_support/inflector/inflections.rb @@ -154,13 +154,13 @@ module ActiveSupport end end - # Add uncountable words that shouldn't be attempted inflected. + # Specifies words that are uncountable and should not be inflected. # # uncountable 'money' # uncountable 'money', 'information' # uncountable %w( money information rice ) def uncountable(*words) - (@uncountables << words).flatten! + @uncountables += words.flatten.map(&:downcase) end # Specifies a humanized form of a string by a regular expression rule or diff --git a/activesupport/lib/active_support/inflector/methods.rb b/activesupport/lib/active_support/inflector/methods.rb index cdee4c2ca5..74b3a7c2a9 100644 --- a/activesupport/lib/active_support/inflector/methods.rb +++ b/activesupport/lib/active_support/inflector/methods.rb @@ -73,7 +73,7 @@ module ActiveSupport string = string.sub(/^(?:#{inflections.acronym_regex}(?=\b|[A-Z_])|\w)/) { $&.downcase } end string.gsub!(/(?:_|(\/))([a-z\d]*)/i) { "#{$1}#{inflections.acronyms[$2] || $2.capitalize}" } - string.gsub!('/', '::') + string.gsub!(/\//, '::') string end @@ -89,8 +89,9 @@ module ActiveSupport # # 'SSLError'.underscore.camelize # => "SslError" def underscore(camel_cased_word) - word = camel_cased_word.to_s.gsub('::', '/') - word.gsub!(/(?:([A-Za-z\d])|^)(#{inflections.acronym_regex})(?=\b|[^a-z])/) { "#{$1}#{$1 && '_'}#{$2.downcase}" } + return camel_cased_word unless camel_cased_word =~ /[A-Z-]|::/ + word = camel_cased_word.to_s.gsub(/::/, '/') + word.gsub!(/(?:(?<=([A-Za-z\d]))|\b)(#{inflections.acronym_regex})(?=\b|[^a-z])/) { "#{$1 && '_'}#{$2.downcase}" } word.gsub!(/([A-Z\d]+)([A-Z][a-z])/,'\1_\2') word.gsub!(/([a-z\d])([A-Z])/,'\1_\2') word.tr!("-", "_") @@ -98,26 +99,46 @@ module ActiveSupport word end - # Capitalizes the first word, turns underscores into spaces, and strips a - # trailing '_id' if present. - # Like +titleize+, this is meant for creating pretty output. + # Tweaks an attribute name for display to end users. + # + # Specifically, +humanize+ performs these transformations: + # + # * Applies human inflection rules to the argument. + # * Deletes leading underscores, if any. + # * Removes a "_id" suffix if present. + # * Replaces underscores with spaces, if any. + # * Downcases all words except acronyms. + # * Capitalizes the first word. # # The capitalization of the first word can be turned off by setting the - # optional parameter +capitalize+ to false. - # By default, this parameter is true. + # +:capitalize+ option to false (default is true). # # humanize('employee_salary') # => "Employee salary" # humanize('author_id') # => "Author" # humanize('author_id', capitalize: false) # => "author" + # humanize('_id') # => "Id" + # + # If "SSL" was defined to be an acronym: + # + # humanize('ssl_error') # => "SSL error" + # def humanize(lower_case_and_underscored_word, options = {}) result = lower_case_and_underscored_word.to_s.dup + inflections.humans.each { |(rule, replacement)| break if result.sub!(rule, replacement) } - result.gsub!(/_id$/, "") + + result.sub!(/\A_+/, '') + result.sub!(/_id\z/, '') result.tr!('_', ' ') - result.gsub!(/([a-z\d]*)/i) { |match| + + result.gsub!(/([a-z\d]*)/i) do |match| "#{inflections.acronyms[match] || match.downcase}" - } - result.gsub!(/^\w/) { $&.upcase } if options.fetch(:capitalize, true) + end + + if options.fetch(:capitalize, true) + result.sub!(/\A\w/) { |match| match.upcase } + end + result end @@ -154,7 +175,7 @@ module ActiveSupport # # Singular names are not handled correctly: # - # 'business'.classify # => "Busines" + # 'calculus'.classify # => "Calculu" def classify(table_name) # strip out any leading schema name camelize(singularize(table_name.to_s.sub(/.*\./, ''))) @@ -171,6 +192,8 @@ module ActiveSupport # # 'ActiveRecord::CoreExtensions::String::Inflections'.demodulize # => "Inflections" # 'Inflections'.demodulize # => "Inflections" + # '::Inflections'.demodulize # => "Inflections" + # ''.demodulize # => "" # # See also +deconstantize+. def demodulize(path) @@ -227,7 +250,7 @@ module ActiveSupport def constantize(camel_cased_word) names = camel_cased_word.split('::') - # Trigger a builtin NameError exception including the ill-formed constant in the message. + # Trigger a built-in 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. @@ -241,8 +264,8 @@ module ActiveSupport next candidate if constant.const_defined?(name, false) next candidate unless Object.const_defined?(name) - # Go down the ancestors to check it it's owned - # directly before we reach Object or the end of ancestors. + # Go down the ancestors to check if it is owned directly. The check + # stops when we reach Object or the end of ancestors tree. constant = constant.ancestors.inject do |const, ancestor| break const if ancestor == Object break ancestor if ancestor.const_defined?(name, false) @@ -280,8 +303,8 @@ module ActiveSupport def safe_constantize(camel_cased_word) 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 + raise if e.name && !(camel_cased_word.to_s.split("::").include?(e.name.to_s) || + 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 @@ -325,10 +348,11 @@ module ActiveSupport private - # Mount a regular expression that will match part by part of the constant. + # Mounts a regular expression, returned as a string to ease interpolation, + # that will match part by part the given constant. # - # const_regexp("Foo::Bar::Baz") # => /Foo(::Bar(::Baz)?)?/ - # const_regexp("::") # => /::/ + # const_regexp("Foo::Bar::Baz") # => "Foo(::Bar(::Baz)?)?" + # const_regexp("::") # => "::" def const_regexp(camel_cased_word) #:nodoc: parts = camel_cased_word.split("::") diff --git a/activesupport/lib/active_support/json/decoding.rb b/activesupport/lib/active_support/json/decoding.rb index 8b5fc70dee..35548f3f56 100644 --- a/activesupport/lib/active_support/json/decoding.rb +++ b/activesupport/lib/active_support/json/decoding.rb @@ -12,7 +12,7 @@ module ActiveSupport class << self # Parses a JSON string (JavaScript Object Notation) into a hash. - # See www.json.org for more info. + # See http://www.json.org for more info. # # ActiveSupport::JSON.decode("{\"team\":\"rails\",\"players\":\"36\"}") # => {"team" => "rails", "players" => "36"} diff --git a/activesupport/lib/active_support/json/encoding.rb b/activesupport/lib/active_support/json/encoding.rb index 2859075e10..48f4967892 100644 --- a/activesupport/lib/active_support/json/encoding.rb +++ b/activesupport/lib/active_support/json/encoding.rb @@ -4,15 +4,15 @@ require 'active_support/core_ext/module/delegation' module ActiveSupport class << self delegate :use_standard_json_time_format, :use_standard_json_time_format=, + :time_precision, :time_precision=, :escape_html_entities_in_json, :escape_html_entities_in_json=, - :encode_big_decimal_as_string, :encode_big_decimal_as_string=, :json_encoder, :json_encoder=, :to => :'ActiveSupport::JSON::Encoding' end module JSON # Dumps objects in JSON (JavaScript Object Notation). - # See www.json.org for more info. + # See http://www.json.org for more info. # # ActiveSupport::JSON.encode({ team: 'rails', players: '36' }) # # => "{\"team\":\"rails\",\"players\":\"36\"}" @@ -60,7 +60,7 @@ module ActiveSupport end # Mark these as private so we don't leak encoding-specific constructs - private_constant :ESCAPED_CHARS, :ESCAPE_REGEX_WITH_HTML_ENTITIES, + private_constant :ESCAPED_CHARS, :ESCAPE_REGEX_WITH_HTML_ENTITIES, :ESCAPE_REGEX_WITHOUT_HTML_ENTITIES, :EscapedString # Convert an object into a "JSON-ready" representation composed of @@ -105,62 +105,19 @@ module ActiveSupport # as a safety measure. attr_accessor :escape_html_entities_in_json + # Sets the precision of encoded time values. + # Defaults to 3 (equivalent to millisecond precision) + attr_accessor :time_precision + # Sets the encoder used by Rails to encode Ruby objects into JSON strings # in +Object#to_json+ and +ActiveSupport::JSON.encode+. attr_accessor :json_encoder - - def encode_big_decimal_as_string=(as_string) - message = \ - "The JSON encoder in Rails 4.1 no longer supports encoding BigDecimals as JSON numbers. Instead, " \ - "the new encoder will always encode them as strings.\n\n" \ - "You are seeing this error because you have 'active_support.encode_big_decimal_as_string' in " \ - "your configuration file. If you have been setting this to true, you can safely remove it from " \ - "your configuration. Otherwise, you should add the 'activesupport-json_encoder' gem to your " \ - "Gemfile in order to restore this functionality." - - raise NotImplementedError, message - end - - def encode_big_decimal_as_string - message = \ - "The JSON encoder in Rails 4.1 no longer supports encoding BigDecimals as JSON numbers. Instead, " \ - "the new encoder will always encode them as strings.\n\n" \ - "You are seeing this error because you are trying to check the value of the related configuration, " \ - "'active_support.encode_big_decimal_as_string'. If your application depends on this option, you should " \ - "add the 'activesupport-json_encoder' gem to your Gemfile. For now, this option will always be true. " \ - "In the future, it will be removed from Rails, so you should stop checking its value." - - ActiveSupport::Deprecation.warn message - - true - end - - # Deprecate CircularReferenceError - def const_missing(name) - if name == :CircularReferenceError - message = "The JSON encoder in Rails 4.1 no longer offers protection from circular references. " \ - "You are seeing this warning because you are rescuing from (or otherwise referencing) " \ - "ActiveSupport::Encoding::CircularReferenceError. In the future, this error will be " \ - "removed from Rails. You should remove these rescue blocks from your code and ensure " \ - "that your data structures are free of circular references so they can be properly " \ - "serialized into JSON.\n\n" \ - "For example, the following Hash contains a circular reference to itself:\n" \ - " h = {}\n" \ - " h['circular'] = h\n" \ - "In this case, calling h.to_json would not work properly." - - ActiveSupport::Deprecation.warn message - - SystemStackError - else - super - end - end end self.use_standard_json_time_format = true self.escape_html_entities_in_json = true self.json_encoder = JSONGemEncoder + self.time_precision = 3 end end end diff --git a/activesupport/lib/active_support/key_generator.rb b/activesupport/lib/active_support/key_generator.rb index 598c46bce5..51d2da3a79 100644 --- a/activesupport/lib/active_support/key_generator.rb +++ b/activesupport/lib/active_support/key_generator.rb @@ -57,18 +57,16 @@ module ActiveSupport # secret they've provided is at least 30 characters in length. def ensure_secret_secure(secret) if secret.blank? - raise ArgumentError, "A secret is required to generate an " + - "integrity hash for cookie session data. Use " + - "config.secret_key_base = \"some secret phrase of at " + - "least #{SECRET_MIN_LENGTH} characters\"" + - "in config/initializers/secret_token.rb" + raise ArgumentError, "A secret is required to generate an integrity hash " \ + "for cookie session data. Set a secret_key_base of at least " \ + "#{SECRET_MIN_LENGTH} characters in config/secrets.yml." end if secret.length < SECRET_MIN_LENGTH - raise ArgumentError, "Secret should be something secure, " + - "like \"#{SecureRandom.hex(16)}\". The value you " + - "provided, \"#{secret}\", is shorter than the minimum length " + - "of #{SECRET_MIN_LENGTH} characters" + raise ArgumentError, "Secret should be something secure, " \ + "like \"#{SecureRandom.hex(16)}\". The value you " \ + "provided, \"#{secret}\", is shorter than the minimum length " \ + "of #{SECRET_MIN_LENGTH} characters." end end end diff --git a/activesupport/lib/active_support/message_encryptor.rb b/activesupport/lib/active_support/message_encryptor.rb index 7773611e11..92ab6fe648 100644 --- a/activesupport/lib/active_support/message_encryptor.rb +++ b/activesupport/lib/active_support/message_encryptor.rb @@ -12,7 +12,7 @@ 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. # - # salt = SecureRandom.random_bytes(64) + # 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..." @@ -40,6 +40,7 @@ module ActiveSupport # Options: # * <tt>:cipher</tt> - Cipher to use. Can be any cipher returned by # <tt>OpenSSL::Cipher.ciphers</tt>. Default is 'aes-256-cbc'. + # * <tt>:digest</tt> - String of digest to use for signing. Default is +SHA1+. # * <tt>:serializer</tt> - Object serializer to use. Default is +Marshal+. def initialize(secret, *signature_key_or_options) options = signature_key_or_options.extract_options! @@ -47,7 +48,7 @@ module ActiveSupport @secret = secret @sign_secret = sign_secret @cipher = options[:cipher] || 'aes-256-cbc' - @verifier = MessageVerifier.new(@sign_secret || @secret, :serializer => NullSerializer) + @verifier = MessageVerifier.new(@sign_secret || @secret, digest: options[:digest] || 'SHA1', serializer: NullSerializer) @serializer = options[:serializer] || Marshal end diff --git a/activesupport/lib/active_support/message_verifier.rb b/activesupport/lib/active_support/message_verifier.rb index 8e6e1dcfeb..eee9bbaead 100644 --- a/activesupport/lib/active_support/message_verifier.rb +++ b/activesupport/lib/active_support/message_verifier.rb @@ -1,5 +1,6 @@ require 'base64' require 'active_support/core_ext/object/blank' +require 'active_support/security_utils' module ActiveSupport # +MessageVerifier+ makes it easy to generate and verify messages which are @@ -27,42 +28,96 @@ module ActiveSupport class InvalidSignature < StandardError; end def initialize(secret, options = {}) + raise ArgumentError, 'Secret should not be nil.' unless secret @secret = secret @digest = options[:digest] || 'SHA1' @serializer = options[:serializer] || Marshal end - def verify(signed_message) - raise InvalidSignature if signed_message.blank? + # Checks if a signed message could have been generated by signing an object + # with the +MessageVerifier+'s secret. + # + # verifier = ActiveSupport::MessageVerifier.new 's3Krit' + # signed_message = verifier.generate 'a private message' + # verifier.valid_message?(signed_message) # => true + # + # tampered_message = signed_message.chop # editing the message invalidates the signature + # verifier.valid_message?(tampered_message) # => false + def valid_message?(signed_message) + return if signed_message.blank? data, digest = signed_message.split("--") - if data.present? && digest.present? && secure_compare(digest, generate_digest(data)) + data.present? && digest.present? && ActiveSupport::SecurityUtils.secure_compare(digest, generate_digest(data)) + end + + # Decodes the signed message using the +MessageVerifier+'s secret. + # + # verifier = ActiveSupport::MessageVerifier.new 's3Krit' + # + # signed_message = verifier.generate 'a private message' + # verifier.verified(signed_message) # => 'a private message' + # + # Returns +nil+ if the message was not signed with the same secret. + # + # other_verifier = ActiveSupport::MessageVerifier.new 'd1ff3r3nt-s3Krit' + # other_verifier.verified(signed_message) # => nil + # + # Returns +nil+ if the message is not Base64-encoded. + # + # invalid_message = "f--46a0120593880c733a53b6dad75b42ddc1c8996d" + # verifier.verified(invalid_message) # => nil + # + # Raises any error raised while decoding the signed message. + # + # incompatible_message = "test--dad7b06c94abba8d46a15fafaef56c327665d5ff" + # verifier.verified(incompatible_message) # => TypeError: incompatible marshal file format + def verified(signed_message) + if valid_message?(signed_message) begin - @serializer.load(::Base64.strict_decode64(data)) + data = signed_message.split("--")[0] + @serializer.load(decode(data)) rescue ArgumentError => argument_error - raise InvalidSignature if argument_error.message =~ %r{invalid base64} + return if argument_error.message =~ %r{invalid base64} raise end - else - raise InvalidSignature end end + # Decodes the signed message using the +MessageVerifier+'s secret. + # + # verifier = ActiveSupport::MessageVerifier.new 's3Krit' + # signed_message = verifier.generate 'a private message' + # + # verifier.verify(signed_message) # => 'a private message' + # + # Raises +InvalidSignature+ if the message was not signed with the same + # secret or was not Base64-encoded. + # + # other_verifier = ActiveSupport::MessageVerifier.new 'd1ff3r3nt-s3Krit' + # other_verifier.verify(signed_message) # => ActiveSupport::MessageVerifier::InvalidSignature + def verify(signed_message) + verified(signed_message) || raise(InvalidSignature) + end + + # Generates a signed message for the provided value. + # + # The message is signed with the +MessageVerifier+'s secret. Without knowing + # the secret, the original value cannot be extracted from the message. + # + # verifier = ActiveSupport::MessageVerifier.new 's3Krit' + # verifier.generate 'a private message' # => "BAhJIhRwcml2YXRlLW1lc3NhZ2UGOgZFVA==--e2d724331ebdee96a10fb99b089508d1c72bd772" def generate(value) - data = ::Base64.strict_encode64(@serializer.dump(value)) + data = encode(@serializer.dump(value)) "#{data}--#{generate_digest(data)}" end private - # constant-time comparison algorithm to prevent timing attacks - def secure_compare(a, b) - return false unless a.bytesize == b.bytesize - - l = a.unpack "C#{a.bytesize}" + def encode(data) + ::Base64.strict_encode64(data) + end - res = 0 - b.each_byte { |byte| res |= byte ^ l.shift } - res == 0 + def decode(data) + ::Base64.strict_decode64(data) end def generate_digest(data) diff --git a/activesupport/lib/active_support/multibyte/unicode.rb b/activesupport/lib/active_support/multibyte/unicode.rb index 84799c2399..35efebc65f 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.3.0' + UNICODE_VERSION = '7.0.0' # The default normalization used for operations that require # normalization. It can be set to any of the normalizations @@ -42,7 +42,6 @@ module ActiveSupport 0x0085, # White_Space # Cc <control-0085> 0x00A0, # White_Space # Zs NO-BREAK SPACE 0x1680, # White_Space # Zs OGHAM SPACE MARK - 0x180E, # White_Space # Zs MONGOLIAN VOWEL SEPARATOR (0x2000..0x200A).to_a, # White_Space # Zs [11] EN QUAD..HAIR SPACE 0x2028, # White_Space # Zl LINE SEPARATOR 0x2029, # White_Space # Zp PARAGRAPH SEPARATOR @@ -212,8 +211,8 @@ module ActiveSupport codepoints end - # Ruby >= 2.1 has String#scrub, which is faster than the workaround used for < 2.1. - if RUBY_VERSION >= '2.1' + # Rubinius' String#scrub, however, doesn't support ASCII-incompatible chars. + if !defined?(Rubinius) # Replaces all ISO-8859-1 or CP1252 characters by their UTF-8 equivalent # resulting in a valid UTF-8 string. # @@ -233,16 +232,16 @@ module ActiveSupport # 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) + reader = Encoding::Converter.new(Encoding::UTF_8, Encoding::UTF_16LE) source = string.dup - out = ''.force_encoding(Encoding::UTF_8_MAC) + out = ''.force_encoding(Encoding::UTF_16LE) 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) + out << error_bytes.encode(Encoding::UTF_16LE, Encoding::Windows_1252, invalid: :replace, undef: :replace) end reader.finish @@ -335,7 +334,7 @@ module ActiveSupport begin @codepoints, @composition_exclusion, @composition_map, @boundary, @cp1252 = File.open(self.class.filename, 'rb') { |f| Marshal.load f.read } rescue => e - raise IOError.new("Couldn't load the Unicode tables for UTF8Handler (#{e.message}), ActiveSupport::Multibyte is unusable") + raise IOError.new("Couldn't load the Unicode tables for UTF8Handler (#{e.message}), ActiveSupport::Multibyte is unusable") end # Redefine the === method so we can write shorter rules for grapheme cluster breaks @@ -367,6 +366,7 @@ module ActiveSupport private def apply_mapping(string, mapping) #:nodoc: + database.codepoints string.each_codepoint.map do |codepoint| cp = database.codepoints[codepoint] if cp and (ncp = cp.send(mapping)) and ncp > 0 @@ -384,7 +384,6 @@ module ActiveSupport def database @database ||= UnicodeDatabase.new end - end end end diff --git a/activesupport/lib/active_support/notifications.rb b/activesupport/lib/active_support/notifications.rb index 7a96c66626..b9f8e1ab2c 100644 --- a/activesupport/lib/active_support/notifications.rb +++ b/activesupport/lib/active_support/notifications.rb @@ -16,7 +16,7 @@ module ActiveSupport # render text: 'Foo' # end # - # That executes the block first and notifies all subscribers once done. + # That first executes the block and then notifies all subscribers once done. # # In the example above +render+ is the name of the event, and the rest is called # the _payload_. The payload is a mechanism that allows instrumenters to pass @@ -141,6 +141,11 @@ module ActiveSupport # # ActiveSupport::Notifications.unsubscribe(subscriber) # + # You can also unsubscribe by passing the name of the subscriber object. Note + # that this will unsubscribe all subscriptions with the given name: + # + # ActiveSupport::Notifications.unsubscribe("render") + # # == Default Queue # # Notifications ships with a queue implementation that consumes and publishes events @@ -173,8 +178,8 @@ module ActiveSupport unsubscribe(subscriber) end - def unsubscribe(args) - notifier.unsubscribe(args) + def unsubscribe(subscriber_or_name) + notifier.unsubscribe(subscriber_or_name) end def instrumenter diff --git a/activesupport/lib/active_support/notifications/fanout.rb b/activesupport/lib/active_support/notifications/fanout.rb index 8f5fa646e8..6bf8c7d5de 100644 --- a/activesupport/lib/active_support/notifications/fanout.rb +++ b/activesupport/lib/active_support/notifications/fanout.rb @@ -25,9 +25,15 @@ module ActiveSupport subscriber end - def unsubscribe(subscriber) + def unsubscribe(subscriber_or_name) synchronize do - @subscribers.reject! { |s| s.matches?(subscriber) } + case subscriber_or_name + when String + @subscribers.reject! { |s| s.matches?(subscriber_or_name) } + else + @subscribers.delete(subscriber_or_name) + end + @listeners_for.clear end end @@ -97,12 +103,11 @@ module ActiveSupport end def subscribed_to?(name) - @pattern === name.to_s + @pattern === name end - def matches?(subscriber_or_name) - self === subscriber_or_name || - @pattern && @pattern === subscriber_or_name + def matches?(name) + @pattern && @pattern === name end end diff --git a/activesupport/lib/active_support/notifications/instrumenter.rb b/activesupport/lib/active_support/notifications/instrumenter.rb index 3a244b34b5..075ddc2382 100644 --- a/activesupport/lib/active_support/notifications/instrumenter.rb +++ b/activesupport/lib/active_support/notifications/instrumenter.rb @@ -57,6 +57,18 @@ module ActiveSupport @duration = nil end + # Returns the difference in milliseconds between when the execution of the + # event started and when it ended. + # + # ActiveSupport::Notifications.subscribe('wait') do |*args| + # @event = ActiveSupport::Notifications::Event.new(*args) + # end + # + # ActiveSupport::Notifications.instrument('wait') do + # sleep 1 + # end + # + # @event.duration # => 1000.138 def duration @duration ||= 1000.0 * (self.end - time) end diff --git a/activesupport/lib/active_support/number_helper.rb b/activesupport/lib/active_support/number_helper.rb index b169e3af01..34439ee8be 100644 --- a/activesupport/lib/active_support/number_helper.rb +++ b/activesupport/lib/active_support/number_helper.rb @@ -232,12 +232,8 @@ module ActiveSupport # number_to_human_size(1234567, precision: 2) # => 1.2 MB # number_to_human_size(483989, precision: 2) # => 470 KB # number_to_human_size(1234567, precision: 2, separator: ',') # => 1,2 MB - # - # Non-significant zeros after the fractional separator are stripped out by - # default (set <tt>:strip_insignificant_zeros</tt> to +false+ to change that): - # - # number_to_human_size(1234567890123, precision: 5) # => "1.1229 TB" - # number_to_human_size(524288000, precision: 5) # => "500 MB" + # number_to_human_size(1234567890123, precision: 5) # => "1.1228 TB" + # number_to_human_size(524288000, precision: 5) # => "500 MB" def number_to_human_size(number, options = {}) NumberToHumanSizeConverter.convert(number, options) end @@ -276,12 +272,12 @@ module ActiveSupport # string containing an i18n scope where to find this hash. It # might have the following keys: # * *integers*: <tt>:unit</tt>, <tt>:ten</tt>, - # *<tt>:hundred</tt>, <tt>:thousand</tt>, <tt>:million</tt>, - # *<tt>:billion</tt>, <tt>:trillion</tt>, - # *<tt>:quadrillion</tt> + # <tt>:hundred</tt>, <tt>:thousand</tt>, <tt>:million</tt>, + # <tt>:billion</tt>, <tt>:trillion</tt>, + # <tt>:quadrillion</tt> # * *fractionals*: <tt>:deci</tt>, <tt>:centi</tt>, - # *<tt>:mili</tt>, <tt>:micro</tt>, <tt>:nano</tt>, - # *<tt>:pico</tt>, <tt>:femto</tt> + # <tt>:mili</tt>, <tt>:micro</tt>, <tt>:nano</tt>, + # <tt>:pico</tt>, <tt>:femto</tt> # * <tt>:format</tt> - Sets the format of the output string # (defaults to "%n %u"). The field types are: # * %u - The quantifier (ex.: 'thousand') @@ -305,12 +301,15 @@ module ActiveSupport # separator: ',', # significant: false) # => "1,2 Million" # + # number_to_human(500000000, precision: 5) # => "500 Million" + # number_to_human(12345012345, significant: false) # => "12.345 Billion" + # # Non-significant zeros after the decimal separator are stripped # out by default (set <tt>:strip_insignificant_zeros</tt> to # +false+ to change that): # - # number_to_human(12345012345, significant_digits: 6) # => "12.345 Billion" - # number_to_human(500000000, precision: 5) # => "500 Million" + # number_to_human(12.00001) # => "12" + # number_to_human(12.00001, strip_insignificant_zeros: false) # => "12.0" # # ==== Custom Unit Quantifiers # diff --git a/activesupport/lib/active_support/number_helper/number_to_currency_converter.rb b/activesupport/lib/active_support/number_helper/number_to_currency_converter.rb index 9ae27a896a..fb5adb574a 100644 --- a/activesupport/lib/active_support/number_helper/number_to_currency_converter.rb +++ b/activesupport/lib/active_support/number_helper/number_to_currency_converter.rb @@ -13,7 +13,7 @@ module ActiveSupport end rounded_number = NumberToRoundedConverter.convert(number, options) - format.gsub('%n', rounded_number).gsub('%u', options[:unit]) + format.gsub(/%n/, rounded_number).gsub(/%u/, options[:unit]) end private diff --git a/activesupport/lib/active_support/number_helper/number_to_delimited_converter.rb b/activesupport/lib/active_support/number_helper/number_to_delimited_converter.rb index 6405afc9a6..d85cc086d7 100644 --- a/activesupport/lib/active_support/number_helper/number_to_delimited_converter.rb +++ b/activesupport/lib/active_support/number_helper/number_to_delimited_converter.rb @@ -13,7 +13,9 @@ module ActiveSupport def parts left, right = number.to_s.split('.') - left.gsub!(DELIMITED_REGEX) { "#{$1}#{options[:delimiter]}" } + left.gsub!(DELIMITED_REGEX) do |digit_to_delimit| + "#{digit_to_delimit}#{options[:delimiter]}" + end [left, right].compact end end diff --git a/activesupport/lib/active_support/number_helper/number_to_human_converter.rb b/activesupport/lib/active_support/number_helper/number_to_human_converter.rb index 9a3dc526ae..6940beb318 100644 --- a/activesupport/lib/active_support/number_helper/number_to_human_converter.rb +++ b/activesupport/lib/active_support/number_helper/number_to_human_converter.rb @@ -59,7 +59,7 @@ module ActiveSupport translate_in_locale("human.decimal_units.units", raise: true) else raise ArgumentError, ":units must be a Hash or String translation scope." - end.keys.map { |e_name| INVERTED_DECIMAL_UNITS[e_name] }.sort_by { |e| -e } + end.keys.map { |e_name| INVERTED_DECIMAL_UNITS[e_name] }.sort_by(&:-@) end end end diff --git a/activesupport/lib/active_support/number_helper/number_to_percentage_converter.rb b/activesupport/lib/active_support/number_helper/number_to_percentage_converter.rb index eafe2844f7..1af294a03e 100644 --- a/activesupport/lib/active_support/number_helper/number_to_percentage_converter.rb +++ b/activesupport/lib/active_support/number_helper/number_to_percentage_converter.rb @@ -5,7 +5,7 @@ module ActiveSupport def convert rounded_number = NumberToRoundedConverter.convert(number, options) - options[:format].gsub('%n', rounded_number) + options[:format].gsub(/%n/, rounded_number) end end end diff --git a/activesupport/lib/active_support/number_helper/number_to_rounded_converter.rb b/activesupport/lib/active_support/number_helper/number_to_rounded_converter.rb index c42354fc83..dcf9a567e8 100644 --- a/activesupport/lib/active_support/number_helper/number_to_rounded_converter.rb +++ b/activesupport/lib/active_support/number_helper/number_to_rounded_converter.rb @@ -12,11 +12,7 @@ module ActiveSupport when Float, String @number = BigDecimal(number.to_s) when Rational - if significant - @number = BigDecimal(number, digit_count(number.to_i) + precision) - else - @number = BigDecimal(number, precision) - end + @number = BigDecimal(number, digit_count(number.to_i) + precision) else @number = number.to_d end @@ -32,13 +28,12 @@ module ActiveSupport end formatted_string = - case rounded_number - when BigDecimal + if BigDecimal === rounded_number && rounded_number.finite? s = rounded_number.to_s('F') + '0'*precision a, b = s.split('.', 2) a + '.' + b[0, precision] else - "%01.#{precision}f" % rounded_number + "%00.#{precision}f" % rounded_number end delimited_number = NumberToDelimitedConverter.convert(formatted_string, options) @@ -64,7 +59,7 @@ module ActiveSupport end def digit_count(number) - (Math.log10(absolute_number(number)) + 1).floor + number.zero? ? 1 : (Math.log10(absolute_number(number)) + 1).floor end def strip_insignificant_zeros diff --git a/activesupport/lib/active_support/option_merger.rb b/activesupport/lib/active_support/option_merger.rb index e55ffd12c3..dea84e437f 100644 --- a/activesupport/lib/active_support/option_merger.rb +++ b/activesupport/lib/active_support/option_merger.rb @@ -12,7 +12,7 @@ module ActiveSupport private def method_missing(method, *arguments, &block) - if arguments.last.is_a?(Proc) + if arguments.first.is_a?(Proc) proc = arguments.pop arguments << lambda { |*args| @options.deep_merge(proc.call(*args)) } else diff --git a/activesupport/lib/active_support/ordered_hash.rb b/activesupport/lib/active_support/ordered_hash.rb index 1a3693f766..4680d5acb7 100644 --- a/activesupport/lib/active_support/ordered_hash.rb +++ b/activesupport/lib/active_support/ordered_hash.rb @@ -28,6 +28,14 @@ module ActiveSupport coder.represent_seq '!omap', map { |k,v| { k => v } } end + def select(*args, &block) + dup.tap { |hash| hash.select!(*args, &block) } + end + + def reject(*args, &block) + dup.tap { |hash| hash.reject!(*args, &block) } + end + def nested_under_indifferent_access self end diff --git a/activesupport/lib/active_support/railtie.rb b/activesupport/lib/active_support/railtie.rb index 133aa6a054..6eba24b569 100644 --- a/activesupport/lib/active_support/railtie.rb +++ b/activesupport/lib/active_support/railtie.rb @@ -13,6 +13,13 @@ module ActiveSupport end end + initializer "active_support.halt_callback_chains_on_return_false", after: :load_config_initializers do |app| + if app.config.active_support.key? :halt_callback_chains_on_return_false + ActiveSupport::Callbacks::CallbackChain.halt_and_display_warning_on_return_false = \ + app.config.active_support.halt_callback_chains_on_return_false + end + end + # Sets the default value for Time.zone # If assigned value cannot be matched to a TimeZone, an exception will be raised. initializer "active_support.initialize_time_zone" do |app| diff --git a/activesupport/lib/active_support/rescuable.rb b/activesupport/lib/active_support/rescuable.rb index a7eba91ac5..1a02acd5b1 100644 --- a/activesupport/lib/active_support/rescuable.rb +++ b/activesupport/lib/active_support/rescuable.rb @@ -60,7 +60,7 @@ module ActiveSupport end klasses.each do |klass| - key = if klass.is_a?(Class) && klass <= Exception + key = if klass.is_a?(Module) && klass.respond_to?(:===) klass.name elsif klass.is_a?(String) klass @@ -101,7 +101,7 @@ module ActiveSupport # itself when rescue_from CONSTANT is executed. klass = self.class.const_get(klass_name) rescue nil klass ||= klass_name.constantize rescue nil - exception.is_a?(klass) if klass + klass === exception if klass end case rescuer diff --git a/activesupport/lib/active_support/security_utils.rb b/activesupport/lib/active_support/security_utils.rb new file mode 100644 index 0000000000..64c4801179 --- /dev/null +++ b/activesupport/lib/active_support/security_utils.rb @@ -0,0 +1,20 @@ +module ActiveSupport + module SecurityUtils + # Constant time string comparison. + # + # The values compared should be of fixed length, such as strings + # that have already been processed by HMAC. This should not be used + # on variable length plaintext strings because it could leak length info + # via timing attacks. + def secure_compare(a, b) + return false unless a.bytesize == b.bytesize + + l = a.unpack "C#{a.bytesize}" + + res = 0 + b.each_byte { |byte| res |= byte ^ l.shift } + res == 0 + end + module_function :secure_compare + end +end diff --git a/activesupport/lib/active_support/subscriber.rb b/activesupport/lib/active_support/subscriber.rb index 4b9b48539f..98be78b41b 100644 --- a/activesupport/lib/active_support/subscriber.rb +++ b/activesupport/lib/active_support/subscriber.rb @@ -64,12 +64,21 @@ module ActiveSupport def add_event_subscriber(event) return if %w{ start finish }.include?(event.to_s) - notifier.subscribe("#{event}.#{namespace}", subscriber) + pattern = "#{event}.#{namespace}" + + # don't add multiple subscribers (eg. if methods are redefined) + return if subscriber.patterns.include?(pattern) + + subscriber.patterns << pattern + notifier.subscribe(pattern, subscriber) end end + attr_reader :patterns # :nodoc: + def initialize @queue_key = [self.class.name, object_id].join "-" + @patterns = [] super end diff --git a/activesupport/lib/active_support/tagged_logging.rb b/activesupport/lib/active_support/tagged_logging.rb index d5c2222d2e..9086a959aa 100644 --- a/activesupport/lib/active_support/tagged_logging.rb +++ b/activesupport/lib/active_support/tagged_logging.rb @@ -43,7 +43,9 @@ module ActiveSupport end def current_tags - Thread.current[:activesupport_tagged_logging_tags] ||= [] + # We use our object ID here to void conflicting with other instances + thread_key = @thread_key ||= "activesupport_tagged_logging_tags:#{object_id}".freeze + Thread.current[thread_key] ||= [] end private diff --git a/activesupport/lib/active_support/test_case.rb b/activesupport/lib/active_support/test_case.rb index 2fb5c04316..c7d6c62129 100644 --- a/activesupport/lib/active_support/test_case.rb +++ b/activesupport/lib/active_support/test_case.rb @@ -9,27 +9,46 @@ require 'active_support/testing/isolation' require 'active_support/testing/constant_lookup' require 'active_support/testing/time_helpers' require 'active_support/core_ext/kernel/reporting' -require 'active_support/deprecation' - -begin - silence_warnings { require 'mocha/setup' } -rescue LoadError -end module ActiveSupport class TestCase < ::Minitest::Test Assertion = Minitest::Assertion - alias_method :method_name, :name + class << self + # Sets the order in which test cases are run. + # + # ActiveSupport::TestCase.test_order = :random # => :random + # + # Valid values are: + # * +:random+ (to run tests in random order) + # * +:parallel+ (to run tests in parallel) + # * +:sorted+ (to run tests alphabetically by method name) + # * +:alpha+ (equivalent to +:sorted+) + def test_order=(new_order) + ActiveSupport.test_order = new_order + end + + # Returns the order in which test cases are run. + # + # ActiveSupport::TestCase.test_order # => :random + # + # Possible values are +:random+, +:parallel+, +:alpha+, +:sorted+. + # Defaults to +:random+. + def test_order + test_order = ActiveSupport.test_order - $tags = {} - def self.for_tag(tag) - yield if $tags[tag] + if test_order.nil? + test_order = :random + self.test_order = test_order + end + + test_order + end + + alias :my_tests_are_order_dependent! :i_suck_and_my_tests_are_order_dependent! end - # FIXME: we have tests that depend on run order, we should fix that and - # remove this method call. - self.i_suck_and_my_tests_are_order_dependent! + alias_method :method_name, :name include ActiveSupport::Testing::TaggedLogging include ActiveSupport::Testing::SetupAndTeardown diff --git a/activesupport/lib/active_support/testing/assertions.rb b/activesupport/lib/active_support/testing/assertions.rb index 76a591bc3b..8b649c193f 100644 --- a/activesupport/lib/active_support/testing/assertions.rb +++ b/activesupport/lib/active_support/testing/assertions.rb @@ -9,7 +9,7 @@ module ActiveSupport # # assert_not nil # => true # assert_not false # => true - # assert_not 'foo' # => 'foo' is not nil or false + # assert_not 'foo' # => Expected "foo" to be nil or false # # An error message can be specified. # @@ -66,7 +66,7 @@ module ActiveSupport exps = expressions.map { |e| e.respond_to?(:call) ? e : lambda { eval(e, block.binding) } } - before = exps.map { |e| e.call } + before = exps.map(&:call) yield diff --git a/activesupport/lib/active_support/testing/constant_lookup.rb b/activesupport/lib/active_support/testing/constant_lookup.rb index 1b2a75c35d..07d477c0db 100644 --- a/activesupport/lib/active_support/testing/constant_lookup.rb +++ b/activesupport/lib/active_support/testing/constant_lookup.rb @@ -36,12 +36,8 @@ module ActiveSupport while names.size > 0 do names.last.sub!(/Test$/, "") begin - constant = names.join("::").constantize + constant = names.join("::").safe_constantize break(constant) if yield(constant) - rescue NoMethodError # subclass of NameError - raise - rescue NameError - # Constant wasn't found, move on ensure names.pop end diff --git a/activesupport/lib/active_support/testing/declarative.rb b/activesupport/lib/active_support/testing/declarative.rb index c349bb5fb1..0bf3643a56 100644 --- a/activesupport/lib/active_support/testing/declarative.rb +++ b/activesupport/lib/active_support/testing/declarative.rb @@ -1,30 +1,6 @@ module ActiveSupport module Testing module Declarative - - def self.extended(klass) #:nodoc: - klass.class_eval do - - unless method_defined?(:describe) - 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 - 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. @@ -34,7 +10,7 @@ module ActiveSupport # end def test(name, &block) test_name = "test_#{name.gsub(/\s+/,'_')}".to_sym - defined = instance_method(test_name) rescue false + defined = method_defined? test_name raise "#{test_name} is already defined in #{self}" if defined if block_given? define_method(test_name, &block) diff --git a/activesupport/lib/active_support/testing/isolation.rb b/activesupport/lib/active_support/testing/isolation.rb index 75ead48376..247df7423b 100644 --- a/activesupport/lib/active_support/testing/isolation.rb +++ b/activesupport/lib/active_support/testing/isolation.rb @@ -1,5 +1,3 @@ -require 'rbconfig' - module ActiveSupport module Testing module Isolation @@ -12,7 +10,7 @@ module ActiveSupport end def self.forking_env? - !ENV["NO_FORK"] && ((RbConfig::CONFIG['host_os'] !~ /mswin|mingw/) && (RUBY_PLATFORM !~ /java/)) + !ENV["NO_FORK"] && Process.respond_to?(:fork) end @@class_setup_mutex = Mutex.new @@ -37,6 +35,8 @@ module ActiveSupport module Forking def run_in_isolation(&blk) read, write = IO.pipe + read.binmode + write.binmode pid = fork do read.close @@ -68,14 +68,24 @@ module ActiveSupport exit! else Tempfile.open("isolation") do |tmpfile| - ENV["ISOLATION_TEST"] = self.class.name - ENV["ISOLATION_OUTPUT"] = tmpfile.path + env = { + ISOLATION_TEST: self.class.name, + ISOLATION_OUTPUT: tmpfile.path + } load_paths = $-I.map {|p| "-I\"#{File.expand_path(p)}\"" }.join(" ") - `#{Gem.ruby} #{load_paths} #{$0} #{ORIG_ARGV.join(" ")}` - - ENV.delete("ISOLATION_TEST") - ENV.delete("ISOLATION_OUTPUT") + orig_args = ORIG_ARGV.join(" ") + test_opts = "-n#{self.class.name}##{self.name}" + command = "#{Gem.ruby} #{load_paths} #{$0} #{orig_args} #{test_opts}" + + # IO.popen lets us pass env in a cross-platform way + child = IO.popen([env, command]) + + begin + Process.wait(child.pid) + rescue Errno::ECHILD # The child process may exit before we wait + nil + end return tmpfile.read.unpack("m")[0] end diff --git a/activesupport/lib/active_support/testing/tagged_logging.rb b/activesupport/lib/active_support/testing/tagged_logging.rb index f4cee64091..843ce4a867 100644 --- a/activesupport/lib/active_support/testing/tagged_logging.rb +++ b/activesupport/lib/active_support/testing/tagged_logging.rb @@ -6,7 +6,7 @@ module ActiveSupport attr_writer :tagged_logger def before_setup - if tagged_logger + if tagged_logger && tagged_logger.info? heading = "#{self.class}: #{name}" divider = '-' * heading.size tagged_logger.info divider diff --git a/activesupport/lib/active_support/testing/time_helpers.rb b/activesupport/lib/active_support/testing/time_helpers.rb index 94230e56ba..8c63815660 100644 --- a/activesupport/lib/active_support/testing/time_helpers.rb +++ b/activesupport/lib/active_support/testing/time_helpers.rb @@ -1,10 +1,48 @@ module ActiveSupport module Testing + class SimpleStubs # :nodoc: + Stub = Struct.new(:object, :method_name, :original_method) + + def initialize + @stubs = {} + end + + def stub_object(object, method_name, return_value) + key = [object.object_id, method_name] + + if stub = @stubs[key] + unstub_object(stub) + end + + new_name = "__simple_stub__#{method_name}" + + @stubs[key] = Stub.new(object, method_name, new_name) + + object.singleton_class.send :alias_method, new_name, method_name + object.define_singleton_method(method_name) { return_value } + end + + def unstub_all! + @stubs.each_value do |stub| + unstub_object(stub) + end + @stubs = {} + end + + private + + def unstub_object(stub) + singleton_class = stub.object.singleton_class + singleton_class.send :undef_method, stub.method_name + singleton_class.send :alias_method, stub.method_name, stub.original_method + singleton_class.send :undef_method, stub.original_method + end + end + # Containing helpers that helps you test passage of time. module TimeHelpers - # Change current time to the time in the future or in the past by a given time difference by - # stubbing +Time.now+ and +Date.today+. Note that the stubs are automatically removed - # at the end of each test. + # Changes current time to the time in the future or in the past by a given time difference by + # stubbing +Time.now+ and +Date.today+. # # Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00 # travel 1.day @@ -23,33 +61,71 @@ module ActiveSupport travel_to Time.now + duration, &block end - # Change current time to the given time by stubbing +Time.now+ and +Date.today+ to return the - # time or date passed into this method. Note that the stubs are automatically removed - # at the end of each test. + # Changes current time to the given time by stubbing +Time.now+ and + # +Date.today+ to return the time or date passed into this method. # # Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00 # travel_to Time.new(2004, 11, 24, 01, 04, 44) # Time.current # => Wed, 24 Nov 2004 01:04:44 EST -05:00 # Date.current # => Wed, 24 Nov 2004 # + # Dates are taken as their timestamp at the beginning of the day in the + # application time zone. <tt>Time.current</tt> returns said timestamp, + # and <tt>Time.now</tt> its equivalent in the system time zone. Similarly, + # <tt>Date.current</tt> returns a date equal to the argument, and + # <tt>Date.today</tt> the date according to <tt>Time.now</tt>, which may + # be different. (Note that you rarely want to deal with <tt>Time.now</tt>, + # or <tt>Date.today</tt>, in order to honor the application time zone + # please always use <tt>Time.current</tt> and <tt>Date.current</tt>.) + # + # Note that the usec for the time passed will be set to 0 to prevent rounding + # errors with external services, like MySQL (which will round instead of floor, + # leading to off-by-one-second errors). + # # This method also accepts a block, which will return the current time back to its original # state at the end of the block: # # Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00 # travel_to Time.new(2004, 11, 24, 01, 04, 44) do - # User.create.created_at # => Wed, 24 Nov 2004 01:04:44 EST -05:00 + # Time.current # => Wed, 24 Nov 2004 01:04:44 EST -05:00 # end # Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00 - def travel_to(date_or_time, &block) - Time.stubs now: date_or_time.to_time - Date.stubs today: date_or_time.to_date + def travel_to(date_or_time) + if date_or_time.is_a?(Date) && !date_or_time.is_a?(DateTime) + now = date_or_time.midnight.to_time + else + now = date_or_time.to_time.change(usec: 0) + end + + simple_stubs.stub_object(Time, :now, now) + simple_stubs.stub_object(Date, :today, now.to_date) if block_given? - block.call - Time.unstub :now - Date.unstub :today + begin + yield + ensure + travel_back + end end end + + # Returns the current time back to its original state, by removing the stubs added by + # `travel` and `travel_to`. + # + # Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00 + # travel_to Time.new(2004, 11, 24, 01, 04, 44) + # Time.current # => Wed, 24 Nov 2004 01:04:44 EST -05:00 + # travel_back + # Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00 + def travel_back + simple_stubs.unstub_all! + end + + private + + def simple_stubs + @simple_stubs ||= SimpleStubs.new + end end end end diff --git a/activesupport/lib/active_support/time.rb b/activesupport/lib/active_support/time.rb index 92a593965e..ea2d3391bd 100644 --- a/activesupport/lib/active_support/time.rb +++ b/activesupport/lib/active_support/time.rb @@ -1,5 +1,3 @@ -require 'active_support' - module ActiveSupport autoload :Duration, 'active_support/duration' autoload :TimeWithZone, 'active_support/time_with_zone' diff --git a/activesupport/lib/active_support/time_with_zone.rb b/activesupport/lib/active_support/time_with_zone.rb index 50db7da9d9..8ddf233b3e 100644 --- a/activesupport/lib/active_support/time_with_zone.rb +++ b/activesupport/lib/active_support/time_with_zone.rb @@ -45,7 +45,7 @@ module ActiveSupport def initialize(utc_time, time_zone, local_time = nil, period = nil) @utc, @time_zone, @time = utc_time, time_zone, local_time - @period = @utc ? period : get_period_and_ensure_valid_local_time + @period = @utc ? period : get_period_and_ensure_valid_local_time(period) end # Returns a Time or DateTime instance that represents the time in +time_zone+. @@ -75,8 +75,8 @@ module ActiveSupport # Returns a <tt>Time.local()</tt> instance of the simultaneous time in your # system's <tt>ENV['TZ']</tt> zone. - def localtime - utc.respond_to?(:getlocal) ? utc.getlocal : utc.to_time.getlocal + def localtime(utc_offset = nil) + utc.respond_to?(:getlocal) ? utc.getlocal(utc_offset) : utc.to_time.getlocal(utc_offset) end alias_method :getlocal, :localtime @@ -121,19 +121,28 @@ module ActiveSupport utc? && alternate_utc_string || TimeZone.seconds_to_utc_offset(utc_offset, colon) end - # Time uses +zone+ to display the time zone abbreviation, so we're - # duck-typing it. + # Returns the time zone abbreviation. + # + # Time.zone = 'Eastern Time (US & Canada)' # => "Eastern Time (US & Canada)" + # Time.zone.now.zone # => "EST" def zone period.zone_identifier.to_s end + # Returns a string of the object's date, time, zone and offset from UTC. + # + # Time.zone.now.httpdate # => "Thu, 04 Dec 2014 11:00:25 EST -05:00" def inspect "#{time.strftime('%a, %d %b %Y %H:%M:%S')} #{zone} #{formatted_offset}" end + # Returns a string of the object's date and time in the ISO 8601 standard + # format. + # + # Time.zone.now.xmlschema # => "2014-12-04T11:02:37-05:00" def xmlschema(fraction_digits = 0) - fraction = if fraction_digits > 0 - (".%06i" % time.usec)[0, fraction_digits + 1] + fraction = if fraction_digits.to_i > 0 + (".%06i" % time.usec)[0, fraction_digits.to_i + 1] end "#{time.strftime("%Y-%m-%dT%H:%M:%S")}#{fraction}#{formatted_offset(true, 'Z')}" @@ -154,7 +163,7 @@ module ActiveSupport # # => "2005/02/01 05:15:10 -1000" def as_json(options = nil) if ActiveSupport::JSON::Encoding.use_standard_json_time_format - xmlschema(3) + xmlschema(ActiveSupport::JSON::Encoding.time_precision) else %(#{time.strftime("%Y/%m/%d %H:%M:%S")} #{formatted_offset(false)}) end @@ -185,28 +194,27 @@ module ActiveSupport end alias_method :rfc822, :rfc2822 - # <tt>:db</tt> format outputs time in UTC; all others output time in local. - # Uses TimeWithZone's +strftime+, so <tt>%Z</tt> and <tt>%z</tt> work correctly. + # Returns a string of the object's date and time. + # Accepts an optional <tt>format</tt>: + # * <tt>:default</tt> - default value, mimics Ruby Time#to_s format. + # * <tt>:db</tt> - format outputs time in UTC :db time. See Time#to_formatted_s(:db). + # * Any key in <tt>Time::DATE_FORMATS</tt> can be used. See active_support/core_ext/time/conversions.rb. def to_s(format = :default) if format == :db utc.to_s(format) elsif formatter = ::Time::DATE_FORMATS[format] formatter.respond_to?(:call) ? formatter.call(self).to_s : strftime(formatter) else - "#{time.strftime("%Y-%m-%d %H:%M:%S")} #{formatted_offset(false, 'UTC')}" # mimicking Ruby 1.9 Time#to_s format + "#{time.strftime("%Y-%m-%d %H:%M:%S")} #{formatted_offset(false, 'UTC')}" # mimicking Ruby Time#to_s format end end alias_method :to_formatted_s, :to_s - # Replaces <tt>%Z</tt> and <tt>%z</tt> directives with +zone+ and - # +formatted_offset+, respectively, before passing to Time#strftime, so - # that zone information is correct + # Replaces <tt>%Z</tt> directive with +zone before passing to Time#strftime, + # so that zone information is correct. def strftime(format) - format = format.gsub('%Z', zone) - .gsub('%z', formatted_offset(false)) - .gsub('%:z', formatted_offset(true)) - .gsub('%::z', formatted_offset(true) + ":00") - time.strftime(format) + format = format.gsub(/((?:\A|[^%])(?:%%)*)%Z/, "\\1#{zone}") + getlocal(utc_offset).strftime(format) end # Use the time in UTC for comparisons. @@ -244,9 +252,23 @@ module ActiveSupport utc.hash end + # Adds an interval of time to the current object's time and return that + # value as a new TimeWithZone object. + # + # Time.zone = 'Eastern Time (US & Canada)' # => 'Eastern Time (US & Canada)' + # now = Time.zone.now # => Sun, 02 Nov 2014 01:26:28 EDT -04:00 + # now + 1000 # => Sun, 02 Nov 2014 01:43:08 EDT -04:00 + # + # If we're adding a Duration of variable length (i.e., years, months, days), + # move forward from #time, otherwise move forward from #utc, for accuracy + # when moving across DST boundaries. + # + # For instance, a time + 24.hours will advance exactly 24 hours, while a + # time + 1.day will advance 23-25 hours, depending on the day. + # + # now + 24.hours # => Mon, 03 Nov 2014 00:26:28 EST -05:00 + # now + 1.day # => Mon, 03 Nov 2014 01:26:28 EST -05:00 def +(other) - # If we're adding a Duration of variable length (i.e., years, months, days), move forward from #time, - # otherwise move forward from #utc, for accuracy when moving across DST boundaries if duration_of_variable_length?(other) method_missing(:+, other) else @@ -254,12 +276,27 @@ module ActiveSupport result.in_time_zone(time_zone) end end + alias_method :since, :+ + # Returns a new TimeWithZone object that represents the difference between + # the current object's time and the +other+ time. + # + # Time.zone = 'Eastern Time (US & Canada)' # => 'Eastern Time (US & Canada)' + # now = Time.zone.now # => Sun, 02 Nov 2014 01:26:28 EST -05:00 + # now - 1000 # => Sun, 02 Nov 2014 01:09:48 EST -05:00 + # + # If subtracting a Duration of variable length (i.e., years, months, days), + # move backward from #time, otherwise move backward from #utc, for accuracy + # when moving across DST boundaries. + # + # For instance, a time - 24.hours will go subtract exactly 24 hours, while a + # time - 1.day will subtract 23-25 hours, depending on the day. + # + # now - 24.hours # => Sat, 01 Nov 2014 02:26:28 EDT -04:00 + # now - 1.day # => Sat, 01 Nov 2014 01:26:28 EDT -04:00 def -(other) - # If we're subtracting a Duration of variable length (i.e., years, months, days), move backwards from #time, - # otherwise move backwards #utc, for accuracy when moving across DST boundaries if other.acts_like?(:time) - utc.to_f - other.to_f + to_time - other.to_time elsif duration_of_variable_length?(other) method_missing(:-, other) else @@ -268,16 +305,6 @@ module ActiveSupport end end - def since(other) - # If we're adding a Duration of variable length (i.e., years, months, days), move forward from #time, - # otherwise move forward from #utc, for accuracy when moving across DST boundaries - if duration_of_variable_length?(other) - method_missing(:since, other) - else - utc.since(other).in_time_zone(time_zone) - end - end - def ago(other) since(-other) end @@ -304,15 +331,27 @@ module ActiveSupport [time.sec, time.min, time.hour, time.day, time.mon, time.year, time.wday, time.yday, dst?, zone] end + # Returns the object's date and time as a floating point number of seconds + # since the Epoch (January 1, 1970 00:00 UTC). + # + # Time.zone.now.to_f # => 1417709320.285418 def to_f utc.to_f end + # Returns the object's date and time as an integer number of seconds + # since the Epoch (January 1, 1970 00:00 UTC). + # + # Time.zone.now.to_i # => 1417709320 def to_i utc.to_i end alias_method :tv_sec, :to_i + # Returns the object's date and time as a rational number of seconds + # since the Epoch (January 1, 1970 00:00 UTC). + # + # Time.zone.now.to_r # => (708854548642709/500000) def to_r utc.to_r end @@ -350,6 +389,14 @@ module ActiveSupport initialize(variables[0].utc, ::Time.find_zone(variables[1]), variables[2].utc) end + # respond_to_missing? is not called in some cases, such as when type conversion is + # performed with Kernel#String + def respond_to?(sym, include_priv = false) + # ensure that we're not going to throw and rescue from NoMethodError in method_missing which is slow + return false if sym.to_sym == :to_str + super + end + # Ensure proxy class responds to all methods that underlying time instance # responds to. def respond_to_missing?(sym, include_priv) @@ -367,12 +414,12 @@ module ActiveSupport end private - def get_period_and_ensure_valid_local_time + def get_period_and_ensure_valid_local_time(period) # we don't want a Time.local instance enforcing its own DST rules as well, # so transfer time values to a utc constructor if necessary @time = transfer_time_values_to_utc_constructor(@time) unless @time.utc? begin - @time_zone.period_for_local(@time) + period || @time_zone.period_for_local(@time) rescue ::TZInfo::PeriodNotFound # time is in the "spring forward" hour gap, so we're moving the time forward one hour and trying again @time += 1.hour @@ -390,7 +437,8 @@ module ActiveSupport def wrap_with_time_zone(time) if time.acts_like?(:time) - self.class.new(nil, time_zone, time) + periods = time_zone.periods_for_local(time) + self.class.new(nil, time_zone, time, periods.include?(period) ? period : nil) elsif time.is_a?(Range) wrap_with_time_zone(time.begin)..wrap_with_time_zone(time.end) else diff --git a/activesupport/lib/active_support/values/time_zone.rb b/activesupport/lib/active_support/values/time_zone.rb index beaac42fa1..728b53849d 100644 --- a/activesupport/lib/active_support/values/time_zone.rb +++ b/activesupport/lib/active_support/values/time_zone.rb @@ -111,9 +111,11 @@ module ActiveSupport "Jerusalem" => "Asia/Jerusalem", "Harare" => "Africa/Harare", "Pretoria" => "Africa/Johannesburg", + "Kaliningrad" => "Europe/Kaliningrad", "Moscow" => "Europe/Moscow", "St. Petersburg" => "Europe/Moscow", - "Volgograd" => "Europe/Moscow", + "Volgograd" => "Europe/Volgograd", + "Samara" => "Europe/Samara", "Kuwait" => "Asia/Kuwait", "Riyadh" => "Asia/Riyadh", "Nairobi" => "Africa/Nairobi", @@ -170,6 +172,7 @@ module ActiveSupport "Guam" => "Pacific/Guam", "Port Moresby" => "Pacific/Port_Moresby", "Magadan" => "Asia/Magadan", + "Srednekolymsk" => "Asia/Srednekolymsk", "Solomon Is." => "Pacific/Guadalcanal", "New Caledonia" => "Pacific/Noumea", "Fiji" => "Pacific/Fiji", @@ -184,20 +187,76 @@ module ActiveSupport } UTC_OFFSET_WITH_COLON = '%s%02d:%02d' - UTC_OFFSET_WITHOUT_COLON = UTC_OFFSET_WITH_COLON.sub(':', '') + UTC_OFFSET_WITHOUT_COLON = UTC_OFFSET_WITH_COLON.tr(':', '') @lazy_zones_map = ThreadSafe::Cache.new - # Assumes self represents an offset from UTC in seconds (as returned from - # Time#utc_offset) and turns this into an +HH:MM formatted string. - # - # TimeZone.seconds_to_utc_offset(-21_600) # => "-06:00" - def self.seconds_to_utc_offset(seconds, colon = true) - format = colon ? UTC_OFFSET_WITH_COLON : UTC_OFFSET_WITHOUT_COLON - sign = (seconds < 0 ? '-' : '+') - hours = seconds.abs / 3600 - minutes = (seconds.abs % 3600) / 60 - format % [sign, hours, minutes] + class << self + # Assumes self represents an offset from UTC in seconds (as returned from + # Time#utc_offset) and turns this into an +HH:MM formatted string. + # + # TimeZone.seconds_to_utc_offset(-21_600) # => "-06:00" + def seconds_to_utc_offset(seconds, colon = true) + format = colon ? UTC_OFFSET_WITH_COLON : UTC_OFFSET_WITHOUT_COLON + sign = (seconds < 0 ? '-' : '+') + hours = seconds.abs / 3600 + minutes = (seconds.abs % 3600) / 60 + format % [sign, hours, minutes] + end + + def find_tzinfo(name) + TZInfo::Timezone.new(MAPPING[name] || name) + end + + alias_method :create, :new + + # Returns a TimeZone instance with the given name, or +nil+ if no + # such TimeZone instance exists. (This exists to support the use of + # this class with the +composed_of+ macro.) + def new(name) + self[name] + end + + # Returns an array of all TimeZone objects. There are multiple + # TimeZone objects per time zone, in many cases, to make it easier + # for users to find their own time zone. + def all + @zones ||= zones_map.values.sort + end + + def zones_map + @zones_map ||= begin + MAPPING.each_key {|place| self[place]} # load all the zones + @lazy_zones_map + end + end + + # Locate a specific time zone object. If the argument is a string, it + # is interpreted to mean the name of the timezone to locate. If it is a + # numeric value it is either the hour offset, or the second offset, of the + # timezone to find. (The first one with that offset will be returned.) + # Returns +nil+ if no such time zone is known to the system. + def [](arg) + case arg + when String + begin + @lazy_zones_map[arg] ||= create(arg) + rescue TZInfo::InvalidTimezoneIdentifier + nil + end + when Numeric, ActiveSupport::Duration + arg *= 3600 if arg.abs <= 13 + all.find { |z| z.utc_offset == arg.to_i } + else + raise ArgumentError, "invalid argument to TimeZone[]: #{arg.inspect}" + end + end + + # A convenience method for returning a collection of TimeZone objects + # for time zones in the USA. + def us_zones + @us_zones ||= all.find_all { |z| z.name =~ /US|Arizona|Indiana|Hawaii|Alaska/ } + end end include Comparable @@ -220,8 +279,8 @@ module ActiveSupport if @utc_offset @utc_offset else - @current_period ||= tzinfo.try(:current_period) - @current_period.try(:utc_offset) + @current_period ||= tzinfo.current_period if tzinfo + @current_period.utc_offset if @current_period end end @@ -282,14 +341,19 @@ module ActiveSupport # # Time.zone.now # => Fri, 31 Dec 1999 14:00:00 HST -10:00 # Time.zone.parse('22:30:00') # => Fri, 31 Dec 1999 22:30:00 HST -10:00 - def parse(str, now=now) + # + # However, if the date component is not provided, but any other upper + # components are supplied, then the day of the month defaults to 1: + # + # Time.zone.parse('Mar 2000') # => Wed, 01 Mar 2000 00:00:00 HST -10:00 + def parse(str, now=now()) parts = Date._parse(str, false) return if parts.empty? time = Time.new( parts.fetch(:year, now.year), parts.fetch(:mon, now.month), - parts.fetch(:mday, now.day), + parts.fetch(:mday, parts[:year] || parts[:mon] ? 1 : now.day), parts.fetch(:hour, 0), parts.fetch(:min, 0), parts.fetch(:sec, 0) + parts.fetch(:sec_fraction, 0), @@ -352,66 +416,13 @@ module ActiveSupport tzinfo.period_for_local(time, dst) end - def self.find_tzinfo(name) - TZInfo::TimezoneProxy.new(MAPPING[name] || name) - end - - class << self - alias_method :create, :new - - # Returns a TimeZone instance with the given name, or +nil+ if no - # such TimeZone instance exists. (This exists to support the use of - # this class with the +composed_of+ macro.) - def new(name) - self[name] - end - - # Returns an array of all TimeZone objects. There are multiple - # TimeZone objects per time zone, in many cases, to make it easier - # for users to find their own time zone. - def all - @zones ||= zones_map.values.sort - end - - def zones_map - @zones_map ||= begin - MAPPING.each_key {|place| self[place]} # load all the zones - @lazy_zones_map - end - end - - # Locate a specific time zone object. If the argument is a string, it - # is interpreted to mean the name of the timezone to locate. If it is a - # numeric value it is either the hour offset, or the second offset, of the - # timezone to find. (The first one with that offset will be returned.) - # Returns +nil+ if no such time zone is known to the system. - def [](arg) - case arg - when String - begin - @lazy_zones_map[arg] ||= create(arg).tap { |tz| tz.utc_offset } - rescue TZInfo::InvalidTimezoneIdentifier - nil - end - when Numeric, ActiveSupport::Duration - arg *= 3600 if arg.abs <= 13 - all.find { |z| z.utc_offset == arg.to_i } - else - raise ArgumentError, "invalid argument to TimeZone[]: #{arg.inspect}" - end - end - - # A convenience method for returning a collection of TimeZone objects - # for time zones in the USA. - def us_zones - @us_zones ||= all.find_all { |z| z.name =~ /US|Arizona|Indiana|Hawaii|Alaska/ } - end + def periods_for_local(time) #:nodoc: + tzinfo.periods_for_local(time) end private - - def time_now - Time.now - end + def time_now + Time.now + end end end diff --git a/activesupport/lib/active_support/values/unicode_tables.dat b/activesupport/lib/active_support/values/unicode_tables.dat Binary files differindex 394ee95f4b..760be4c07a 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 b3f0e7198d..fe03984546 100644 --- a/activesupport/lib/active_support/version.rb +++ b/activesupport/lib/active_support/version.rb @@ -1,11 +1,8 @@ +require_relative 'gem_version' + module ActiveSupport - # Returns the version of the currently loaded ActiveSupport as a Gem::Version + # Returns the version of the currently loaded ActiveSupport as a <tt>Gem::Version</tt> def self.version - Gem::Version.new "4.1.0.beta1" - end - - module VERSION #:nodoc: - MAJOR, MINOR, TINY, PRE = ActiveSupport.version.segments - STRING = ActiveSupport.version.to_s + gem_version end end diff --git a/activesupport/lib/active_support/xml_mini.rb b/activesupport/lib/active_support/xml_mini.rb index d082a0a499..009ee4db90 100644 --- a/activesupport/lib/active_support/xml_mini.rb +++ b/activesupport/lib/active_support/xml_mini.rb @@ -1,7 +1,9 @@ require 'time' require 'base64' +require 'bigdecimal' require 'active_support/core_ext/module/delegation' require 'active_support/core_ext/string/inflections' +require 'active_support/core_ext/date_time/calculations' module ActiveSupport # = XmlMini @@ -56,13 +58,13 @@ module ActiveSupport # TODO use regexp instead of Date.parse unless defined?(PARSING) PARSING = { - "symbol" => Proc.new { |symbol| symbol.to_sym }, + "symbol" => Proc.new { |symbol| symbol.to_s.to_sym }, "date" => Proc.new { |date| ::Date.parse(date) }, "datetime" => Proc.new { |time| Time.xmlschema(time).utc rescue ::DateTime.parse(time).utc }, "integer" => Proc.new { |integer| integer.to_i }, "float" => Proc.new { |float| float.to_f }, "decimal" => Proc.new { |number| BigDecimal(number) }, - "boolean" => Proc.new { |boolean| %w(1 true).include?(boolean.strip) }, + "boolean" => Proc.new { |boolean| %w(1 true).include?(boolean.to_s.strip) }, "string" => Proc.new { |string| string.to_s }, "yaml" => Proc.new { |yaml| YAML::load(yaml) rescue yaml }, "base64Binary" => Proc.new { |bin| ::Base64.decode64(bin) }, diff --git a/activesupport/lib/active_support/xml_mini/jdom.rb b/activesupport/lib/active_support/xml_mini/jdom.rb index 27c64c4dca..f303daa1a7 100644 --- a/activesupport/lib/active_support/xml_mini/jdom.rb +++ b/activesupport/lib/active_support/xml_mini/jdom.rb @@ -141,7 +141,7 @@ module ActiveSupport (0...attributes.length).each do |i| attribute_hash[CONTENT_KEY] ||= '' attribute_hash[attributes.item(i).name] = attributes.item(i).value - end + end attribute_hash end diff --git a/activesupport/test/abstract_unit.rb b/activesupport/test/abstract_unit.rb index 0b393e0c7a..f65ec962f9 100644 --- a/activesupport/test/abstract_unit.rb +++ b/activesupport/test/abstract_unit.rb @@ -36,3 +36,10 @@ end def jruby_skip(message = '') skip message if defined?(JRUBY_VERSION) end + +require 'mocha/setup' # FIXME: stop using mocha + +# FIXME: we have tests that depend on run order, we should fix that and +# remove this method call. +require 'active_support/test_case' +ActiveSupport::TestCase.test_order = :sorted diff --git a/activesupport/test/autoload_test.rb b/activesupport/test/autoload_test.rb index 7d02d835a8..c18b007612 100644 --- a/activesupport/test/autoload_test.rb +++ b/activesupport/test/autoload_test.rb @@ -11,6 +11,11 @@ class TestAutoloadModule < ActiveSupport::TestCase end end + def setup + @some_class_path = File.expand_path("test/fixtures/autoload/some_class.rb") + @another_class_path = File.expand_path("test/fixtures/autoload/another_class.rb") + end + test "the autoload module works like normal autoload" do module ::Fixtures::Autoload autoload :SomeClass, "fixtures/autoload/some_class" @@ -21,10 +26,12 @@ class TestAutoloadModule < ActiveSupport::TestCase test "when specifying an :eager constant it still works like normal autoload by default" do module ::Fixtures::Autoload - autoload :SomeClass, "fixtures/autoload/some_class" + eager_autoload do + autoload :SomeClass, "fixtures/autoload/some_class" + end end - assert !$LOADED_FEATURES.include?("fixtures/autoload/some_class.rb") + assert !$LOADED_FEATURES.include?(@some_class_path) assert_nothing_raised { ::Fixtures::Autoload::SomeClass } end @@ -33,16 +40,20 @@ class TestAutoloadModule < ActiveSupport::TestCase autoload :SomeClass end - assert !$LOADED_FEATURES.include?("fixtures/autoload/some_class.rb") + assert !$LOADED_FEATURES.include?(@some_class_path) assert_nothing_raised { ::Fixtures::Autoload::SomeClass } end test "the location of :eager autoloaded constants defaults to :name.underscore" do module ::Fixtures::Autoload - autoload :SomeClass + eager_autoload do + autoload :SomeClass + end end + assert !$LOADED_FEATURES.include?(@some_class_path) ::Fixtures::Autoload.eager_load! + assert $LOADED_FEATURES.include?(@some_class_path) assert_nothing_raised { ::Fixtures::Autoload::SomeClass } end @@ -53,7 +64,7 @@ class TestAutoloadModule < ActiveSupport::TestCase end end - assert !$LOADED_FEATURES.include?("fixtures/autoload/another_class.rb") + assert !$LOADED_FEATURES.include?(@another_class_path) assert_nothing_raised { ::Fixtures::AnotherClass } end @@ -64,7 +75,7 @@ class TestAutoloadModule < ActiveSupport::TestCase end end - assert !$LOADED_FEATURES.include?("fixtures/autoload/another_class.rb") + assert !$LOADED_FEATURES.include?(@another_class_path) assert_nothing_raised { ::Fixtures::AnotherClass } end end
\ No newline at end of file diff --git a/activesupport/test/autoloading_fixtures/typo.rb b/activesupport/test/autoloading_fixtures/typo.rb new file mode 100644 index 0000000000..8e047f5fd4 --- /dev/null +++ b/activesupport/test/autoloading_fixtures/typo.rb @@ -0,0 +1,2 @@ +TypO = 1 + diff --git a/activesupport/test/caching_test.rb b/activesupport/test/caching_test.rb index c3c65cf805..98f3ea7a14 100644 --- a/activesupport/test/caching_test.rb +++ b/activesupport/test/caching_test.rb @@ -60,36 +60,25 @@ class CacheKeyTest < ActiveSupport::TestCase end def test_expand_cache_key_with_rails_cache_id - begin - ENV['RAILS_CACHE_ID'] = 'c99' + with_env('RAILS_CACHE_ID' => 'c99') do assert_equal 'c99/foo', ActiveSupport::Cache.expand_cache_key(:foo) assert_equal 'c99/foo', ActiveSupport::Cache.expand_cache_key([:foo]) assert_equal 'c99/foo/bar', ActiveSupport::Cache.expand_cache_key([:foo, :bar]) assert_equal 'nm/c99/foo', ActiveSupport::Cache.expand_cache_key(:foo, :nm) assert_equal 'nm/c99/foo', ActiveSupport::Cache.expand_cache_key([:foo], :nm) assert_equal 'nm/c99/foo/bar', ActiveSupport::Cache.expand_cache_key([:foo, :bar], :nm) - ensure - ENV['RAILS_CACHE_ID'] = nil end end def test_expand_cache_key_with_rails_app_version - begin - ENV['RAILS_APP_VERSION'] = 'rails3' + with_env('RAILS_APP_VERSION' => 'rails3') do assert_equal 'rails3/foo', ActiveSupport::Cache.expand_cache_key(:foo) - ensure - ENV['RAILS_APP_VERSION'] = nil end end def test_expand_cache_key_rails_cache_id_should_win_over_rails_app_version - begin - ENV['RAILS_CACHE_ID'] = 'c99' - ENV['RAILS_APP_VERSION'] = 'rails3' + with_env('RAILS_CACHE_ID' => 'c99', 'RAILS_APP_VERSION' => 'rails3') do assert_equal 'c99/foo', ActiveSupport::Cache.expand_cache_key(:foo) - ensure - ENV['RAILS_CACHE_ID'] = nil - ENV['RAILS_APP_VERSION'] = nil end end @@ -124,6 +113,16 @@ class CacheKeyTest < ActiveSupport::TestCase def test_expand_cache_key_of_array_like_object assert_equal 'foo/bar/baz', ActiveSupport::Cache.expand_cache_key(%w{foo bar baz}.to_enum) end + + private + + def with_env(kv) + old_values = {} + kv.each { |key, value| old_values[key], ENV[key] = ENV[key], value } + yield + ensure + old_values.each { |key, value| ENV[key] = value} + end end class CacheStoreSettingTest < ActiveSupport::TestCase @@ -297,20 +296,21 @@ module CacheStoreBehavior @cache.write('foo', 'bar') @cache.write('fud', 'biz') - values = @cache.fetch_multi('foo', 'fu', 'fud') {|value| value * 2 } + values = @cache.fetch_multi('foo', 'fu', 'fud') { |value| value * 2 } - assert_equal(["bar", "fufu", "biz"], values) - assert_equal("fufu", @cache.read('fu')) + assert_equal({ 'foo' => 'bar', 'fu' => 'fufu', 'fud' => '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") + foo = stub(:title => 'FOO!', :cache_key => 'foo') + bar = stub(:cache_key => 'bar') - @cache.write('bar', "BAM!") + @cache.write('bar', 'BAM!') - values = @cache.fetch_multi(foo, bar) {|object| object.title } - assert_equal(["FOO!", "BAM!"], values) + values = @cache.fetch_multi(foo, bar) { |object| object.title } + + assert_equal({ foo => 'FOO!', bar => 'BAM!' }, values) end def test_read_and_write_compressed_small_data @@ -691,6 +691,11 @@ class FileStoreTest < ActiveSupport::TestCase assert File.exist?(filepath) end + def test_long_keys + @cache.write("a"*10000, 1) + assert_equal 1, @cache.read("a"*10000) + end + def test_key_transformation key = @cache.send(:key_file_path, "views/index?id=1") assert_equal "views/index?id=1", @cache.send(:file_path_key, key) @@ -934,8 +939,8 @@ class MemCacheStoreTest < ActiveSupport::TestCase def test_read_should_return_a_different_object_id_each_time_it_is_called @cache.write('foo', 'bar') - assert_not_equal @cache.read('foo').object_id, @cache.read('foo').object_id value = @cache.read('foo') + assert_not_equal value.object_id, @cache.read('foo').object_id value << 'bingo' assert_not_equal value, @cache.read('foo') end @@ -1042,30 +1047,4 @@ class CacheEntryTest < ActiveSupport::TestCase assert_equal value, entry.value assert_equal value.bytesize, entry.size end - - 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_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_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 end diff --git a/activesupport/test/callbacks_test.rb b/activesupport/test/callbacks_test.rb index 32c2dfdfc0..f6abef8cee 100644 --- a/activesupport/test/callbacks_test.rb +++ b/activesupport/test/callbacks_test.rb @@ -49,7 +49,7 @@ module CallbacksTest def self.before(model) model.history << [:before_save, :class] end - + def self.after(model) model.history << [:after_save, :class] end @@ -501,21 +501,18 @@ module CallbacksTest end end - class CallbackTerminator + class AbstractCallbackTerminator include ActiveSupport::Callbacks - define_callbacks :save, :terminator => ->(_,result) { result == :halt } - - set_callback :save, :before, :first - set_callback :save, :before, :second - set_callback :save, :around, :around_it - set_callback :save, :before, :third - set_callback :save, :after, :first - set_callback :save, :around, :around_it - set_callback :save, :after, :second - set_callback :save, :around, :around_it - set_callback :save, :after, :third - + def self.set_save_callbacks + set_callback :save, :before, :first + set_callback :save, :before, :second + set_callback :save, :around, :around_it + set_callback :save, :before, :third + set_callback :save, :after, :first + set_callback :save, :around, :around_it + set_callback :save, :after, :third + end attr_reader :history, :saved, :halted def initialize @@ -552,6 +549,39 @@ module CallbacksTest end end + class CallbackTerminator < AbstractCallbackTerminator + define_callbacks :save, terminator: ->(_, result_lambda) { result_lambda.call == :halt } + set_save_callbacks + end + + class CallbackTerminatorSkippingAfterCallbacks < AbstractCallbackTerminator + define_callbacks :save, terminator: ->(_, result_lambda) { result_lambda.call == :halt }, + skip_after_callbacks_if_terminated: true + set_save_callbacks + end + + class CallbackDefaultTerminator < AbstractCallbackTerminator + define_callbacks :save + + def second + @history << "second" + throw(:abort) + end + + set_save_callbacks + end + + class CallbackFalseTerminator < AbstractCallbackTerminator + define_callbacks :save + + def second + @history << "second" + false + end + + set_save_callbacks + end + class CallbackObject def before(caller) caller.record << "before" @@ -688,10 +718,10 @@ module CallbacksTest end class CallbackTerminatorTest < ActiveSupport::TestCase - def test_termination + def test_termination_skips_following_before_and_around_callbacks terminator = CallbackTerminator.new terminator.save - assert_equal ["first", "second", "third", "second", "first"], terminator.history + assert_equal ["first", "second", "third", "first"], terminator.history end def test_termination_invokes_hook @@ -707,6 +737,73 @@ module CallbacksTest end end + class CallbackTerminatorSkippingAfterCallbacksTest < ActiveSupport::TestCase + def test_termination_skips_after_callbacks + terminator = CallbackTerminatorSkippingAfterCallbacks.new + terminator.save + assert_equal ["first", "second"], terminator.history + end + end + + class CallbackDefaultTerminatorTest < ActiveSupport::TestCase + def test_default_termination + terminator = CallbackDefaultTerminator.new + terminator.save + assert_equal ["first", "second", "third", "first"], terminator.history + end + + def test_default_termination_invokes_hook + terminator = CallbackDefaultTerminator.new + terminator.save + assert_equal :second, terminator.halted + end + + def test_block_never_called_if_abort_is_thrown + obj = CallbackDefaultTerminator.new + obj.save + assert !obj.saved + end + end + + class CallbackFalseTerminatorWithoutConfigTest < ActiveSupport::TestCase + def test_returning_false_halts_callback_if_config_variable_is_not_set + obj = CallbackFalseTerminator.new + assert_deprecated do + obj.save + assert_equal :second, obj.halted + assert !obj.saved + end + end + end + + class CallbackFalseTerminatorWithConfigTrueTest < ActiveSupport::TestCase + def setup + ActiveSupport::Callbacks::CallbackChain.halt_and_display_warning_on_return_false = true + end + + def test_returning_false_halts_callback_if_config_variable_is_true + obj = CallbackFalseTerminator.new + assert_deprecated do + obj.save + assert_equal :second, obj.halted + assert !obj.saved + end + end + end + + class CallbackFalseTerminatorWithConfigFalseTest < ActiveSupport::TestCase + def setup + ActiveSupport::Callbacks::CallbackChain.halt_and_display_warning_on_return_false = false + end + + def test_returning_false_does_not_halt_callback_if_config_variable_is_false + obj = CallbackFalseTerminator.new + obj.save + assert_equal nil, obj.halted + assert obj.saved + end + end + class HyphenatedKeyTest < ActiveSupport::TestCase def test_save obj = HyphenatedCallbacks.new diff --git a/activesupport/test/clean_backtrace_test.rb b/activesupport/test/clean_backtrace_test.rb index dd67a45cf6..05580352a9 100644 --- a/activesupport/test/clean_backtrace_test.rb +++ b/activesupport/test/clean_backtrace_test.rb @@ -34,6 +34,11 @@ class BacktraceCleanerSilencerTest < ActiveSupport::TestCase [ "/other/class.rb" ], @bc.clean([ "/mongrel/class.rb", "/other/class.rb", "/mongrel/stuff.rb" ]) end + + test "backtrace cleaner should allow removing silencer" do + @bc.remove_silencers! + assert_equal ["/mongrel/stuff.rb"], @bc.clean(["/mongrel/stuff.rb"]) + end end class BacktraceCleanerMultipleSilencersTest < ActiveSupport::TestCase diff --git a/activesupport/test/concern_test.rb b/activesupport/test/concern_test.rb index a74ee880b2..60bd8a06aa 100644 --- a/activesupport/test/concern_test.rb +++ b/activesupport/test/concern_test.rb @@ -5,7 +5,7 @@ class ConcernTest < ActiveSupport::TestCase module Baz extend ActiveSupport::Concern - module ClassMethods + class_methods do def baz "baz" end @@ -33,6 +33,12 @@ class ConcernTest < ActiveSupport::TestCase include Baz + module ClassMethods + def baz + "bar's baz + " + super + end + end + def bar "bar" end @@ -73,7 +79,7 @@ class ConcernTest < ActiveSupport::TestCase @klass.send(:include, Bar) assert_equal "bar", @klass.new.bar assert_equal "bar+baz", @klass.new.baz - assert_equal "baz", @klass.baz + assert_equal "bar's baz + baz", @klass.baz assert @klass.included_modules.include?(ConcernTest::Bar) end diff --git a/activesupport/test/constantize_test_cases.rb b/activesupport/test/constantize_test_cases.rb index bbeb710a0c..366e4e5ef0 100644 --- a/activesupport/test/constantize_test_cases.rb +++ b/activesupport/test/constantize_test_cases.rb @@ -1,3 +1,5 @@ +require 'dependencies_test_helpers' + module Ace module Base class Case @@ -23,17 +25,30 @@ class Object end module ConstantizeTestCases + include DependenciesTestHelpers + def run_constantize_tests_on assert_equal Ace::Base::Case, yield("Ace::Base::Case") assert_equal Ace::Base::Case, yield("::Ace::Base::Case") assert_equal Ace::Base::Case::Dice, yield("Ace::Base::Case::Dice") + assert_equal Ace::Base::Case::Dice, yield("Ace::Base::Fase::Dice") assert_equal Ace::Base::Fase::Dice, yield("Ace::Base::Fase::Dice") + assert_equal Ace::Gas::Case, yield("Ace::Gas::Case") assert_equal Ace::Gas::Case::Dice, yield("Ace::Gas::Case::Dice") + assert_equal Ace::Base::Case::Dice, yield("Ace::Gas::Case::Dice") + assert_equal Case::Dice, yield("Case::Dice") + assert_equal AddtlGlobalConstants::Case::Dice, yield("Case::Dice") + assert_equal Object::AddtlGlobalConstants::Case::Dice, yield("Case::Dice") + assert_equal Case::Dice, yield("Object::Case::Dice") + assert_equal AddtlGlobalConstants::Case::Dice, yield("Object::Case::Dice") + assert_equal Object::AddtlGlobalConstants::Case::Dice, yield("Case::Dice") + assert_equal ConstantizeTestCases, yield("ConstantizeTestCases") assert_equal ConstantizeTestCases, yield("::ConstantizeTestCases") + assert_raises(NameError) { yield("UnknownClass") } assert_raises(NameError) { yield("UnknownClass::Ace") } assert_raises(NameError) { yield("UnknownClass::Ace::Base") } @@ -45,6 +60,19 @@ module ConstantizeTestCases assert_raises(NameError) { yield("Ace::Gas::ConstantizeTestCases") } assert_raises(NameError) { yield("") } assert_raises(NameError) { yield("::") } + assert_raises(NameError) { yield("Ace::gas") } + + assert_raises(NameError) do + with_autoloading_fixtures do + yield("RaisesNameError") + end + end + + assert_raises(NoMethodError) do + with_autoloading_fixtures do + yield("RaisesNoMethodError") + end + end end def run_safe_constantize_tests_on @@ -71,5 +99,18 @@ module ConstantizeTestCases assert_nil yield("Ace::Gas::Base") assert_nil yield("Ace::Gas::ConstantizeTestCases") assert_nil yield("#<Class:0x7b8b718b>::Nested_1") + assert_nil yield("Ace::gas") + + assert_raises(NameError) do + with_autoloading_fixtures do + yield("RaisesNameError") + end + end + + assert_raises(NoMethodError) do + with_autoloading_fixtures do + yield("RaisesNoMethodError") + end + end end end diff --git a/activesupport/test/core_ext/array/access_test.rb b/activesupport/test/core_ext/array/access_test.rb new file mode 100644 index 0000000000..f14f64421d --- /dev/null +++ b/activesupport/test/core_ext/array/access_test.rb @@ -0,0 +1,30 @@ +require 'abstract_unit' +require 'active_support/core_ext/array' + +class AccessTest < ActiveSupport::TestCase + def test_from + assert_equal %w( a b c d ), %w( a b c d ).from(0) + assert_equal %w( c d ), %w( a b c d ).from(2) + assert_equal %w(), %w( a b c d ).from(10) + assert_equal %w( d e ), %w( a b c d e ).from(-2) + assert_equal %w(), %w( a b c d e ).from(-10) + end + + def test_to + assert_equal %w( a ), %w( a b c d ).to(0) + assert_equal %w( a b c ), %w( a b c d ).to(2) + assert_equal %w( a b c d ), %w( a b c d ).to(10) + assert_equal %w( a b c ), %w( a b c d ).to(-2) + assert_equal %w(), %w( a b c ).to(-10) + end + + def test_specific_accessor + array = (1..42).to_a + + assert_equal array[1], array.second + assert_equal array[2], array.third + assert_equal array[3], array.fourth + assert_equal array[4], array.fifth + assert_equal array[41], array.forty_two + end +end diff --git a/activesupport/test/core_ext/array/conversions_test.rb b/activesupport/test/core_ext/array/conversions_test.rb new file mode 100644 index 0000000000..577b889410 --- /dev/null +++ b/activesupport/test/core_ext/array/conversions_test.rb @@ -0,0 +1,197 @@ +require 'abstract_unit' +require 'active_support/core_ext/array' +require 'active_support/core_ext/big_decimal' +require 'active_support/core_ext/hash' +require 'active_support/core_ext/string' + +class ToSentenceTest < ActiveSupport::TestCase + def test_plain_array_to_sentence + assert_equal "", [].to_sentence + assert_equal "one", ['one'].to_sentence + assert_equal "one and two", ['one', 'two'].to_sentence + assert_equal "one, two, and three", ['one', 'two', 'three'].to_sentence + end + + def test_to_sentence_with_words_connector + assert_equal "one two, and three", ['one', 'two', 'three'].to_sentence(words_connector: ' ') + assert_equal "one & two, and three", ['one', 'two', 'three'].to_sentence(words_connector: ' & ') + assert_equal "onetwo, and three", ['one', 'two', 'three'].to_sentence(words_connector: nil) + end + + def test_to_sentence_with_last_word_connector + assert_equal "one, two, and also three", ['one', 'two', 'three'].to_sentence(last_word_connector: ', and also ') + assert_equal "one, twothree", ['one', 'two', 'three'].to_sentence(last_word_connector: nil) + assert_equal "one, two three", ['one', 'two', 'three'].to_sentence(last_word_connector: ' ') + assert_equal "one, two and three", ['one', 'two', 'three'].to_sentence(last_word_connector: ' and ') + end + + def test_two_elements + assert_equal "one and two", ['one', 'two'].to_sentence + assert_equal "one two", ['one', 'two'].to_sentence(two_words_connector: ' ') + end + + def test_one_element + assert_equal "one", ['one'].to_sentence + end + + def test_one_element_not_same_object + elements = ["one"] + assert_not_equal elements[0].object_id, elements.to_sentence.object_id + end + + def test_one_non_string_element + assert_equal '1', [1].to_sentence + end + + def test_does_not_modify_given_hash + options = { words_connector: ' ' } + 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 + + def test_with_invalid_options + exception = assert_raise ArgumentError do + ['one', 'two'].to_sentence(passing: 'invalid option') + end + + assert_equal exception.message, "Unknown key: :passing. Valid keys are: :words_connector, :two_words_connector, :last_word_connector, :locale" + end +end + +class ToSTest < ActiveSupport::TestCase + class TestDB + @@counter = 0 + def id + @@counter += 1 + end + end + + def test_to_s_db + collection = [TestDB.new, TestDB.new, TestDB.new] + + assert_equal "null", [].to_s(:db) + assert_equal "1,2,3", collection.to_s(:db) + end +end + +class ToXmlTest < ActiveSupport::TestCase + def test_to_xml_with_hash_elements + xml = [ + { name: "David", age: 26, age_in_millis: 820497600000 }, + { name: "Jason", age: 31, age_in_millis: BigDecimal.new('1.0') } + ].to_xml(skip_instruct: true, indent: 0) + + assert_equal '<objects type="array"><object>', xml.first(30) + assert xml.include?(%(<age type="integer">26</age>)), xml + assert xml.include?(%(<age-in-millis type="integer">820497600000</age-in-millis>)), xml + assert xml.include?(%(<name>David</name>)), xml + assert xml.include?(%(<age type="integer">31</age>)), xml + assert xml.include?(%(<age-in-millis type="decimal">1.0</age-in-millis>)), xml + assert xml.include?(%(<name>Jason</name>)), xml + end + + def test_to_xml_with_non_hash_elements + xml = [1, 2, 3].to_xml(skip_instruct: true, indent: 0) + + assert_equal '<fixnums type="array"><fixnum', xml.first(29) + assert xml.include?(%(<fixnum type="integer">2</fixnum>)), xml + end + + def test_to_xml_with_non_hash_different_type_elements + xml = [1, 2.0, '3'].to_xml(skip_instruct: true, indent: 0) + + assert_equal '<objects type="array"><object', xml.first(29) + assert xml.include?(%(<object type="integer">1</object>)), xml + assert xml.include?(%(<object type="float">2.0</object>)), xml + assert xml.include?(%(object>3</object>)), xml + end + + def test_to_xml_with_dedicated_name + xml = [ + { name: "David", age: 26, age_in_millis: 820497600000 }, { name: "Jason", age: 31 } + ].to_xml(skip_instruct: true, indent: 0, root: "people") + + assert_equal '<people type="array"><person>', xml.first(29) + end + + def test_to_xml_with_options + xml = [ + { name: "David", street_address: "Paulina" }, { name: "Jason", street_address: "Evergreen" } + ].to_xml(skip_instruct: true, skip_types: true, indent: 0) + + assert_equal "<objects><object>", xml.first(17) + assert xml.include?(%(<street-address>Paulina</street-address>)) + assert xml.include?(%(<name>David</name>)) + assert xml.include?(%(<street-address>Evergreen</street-address>)) + assert xml.include?(%(<name>Jason</name>)) + end + + def test_to_xml_with_indent_set + xml = [ + { name: "David", street_address: "Paulina" }, { name: "Jason", street_address: "Evergreen" } + ].to_xml(skip_instruct: true, skip_types: true, indent: 4) + + assert_equal "<objects>\n <object>", xml.first(22) + assert xml.include?(%(\n <street-address>Paulina</street-address>)) + assert xml.include?(%(\n <name>David</name>)) + assert xml.include?(%(\n <street-address>Evergreen</street-address>)) + assert xml.include?(%(\n <name>Jason</name>)) + end + + def test_to_xml_with_dasherize_false + xml = [ + { name: "David", street_address: "Paulina" }, { name: "Jason", street_address: "Evergreen" } + ].to_xml(skip_instruct: true, skip_types: true, indent: 0, dasherize: false) + + assert_equal "<objects><object>", xml.first(17) + assert xml.include?(%(<street_address>Paulina</street_address>)) + assert xml.include?(%(<street_address>Evergreen</street_address>)) + end + + def test_to_xml_with_dasherize_true + xml = [ + { name: "David", street_address: "Paulina" }, { name: "Jason", street_address: "Evergreen" } + ].to_xml(skip_instruct: true, skip_types: true, indent: 0, dasherize: true) + + assert_equal "<objects><object>", xml.first(17) + assert xml.include?(%(<street-address>Paulina</street-address>)) + assert xml.include?(%(<street-address>Evergreen</street-address>)) + end + + def test_to_xml_with_instruct + xml = [ + { name: "David", age: 26, age_in_millis: 820497600000 }, + { name: "Jason", age: 31, age_in_millis: BigDecimal.new('1.0') } + ].to_xml(skip_instruct: false, indent: 0) + + assert_match(/^<\?xml [^>]*/, xml) + assert_equal 0, xml.rindex(/<\?xml /) + end + + def test_to_xml_with_block + xml = [ + { name: "David", age: 26, age_in_millis: 820497600000 }, + { name: "Jason", age: 31, age_in_millis: BigDecimal.new('1.0') } + ].to_xml(skip_instruct: true, indent: 0) do |builder| + builder.count 2 + end + + assert xml.include?(%(<count>2</count>)), xml + end + + def test_to_xml_with_empty + xml = [].to_xml + assert_match(/type="array"\/>/, xml) + end + + def test_to_xml_dups_options + options = { skip_instruct: true } + [].to_xml(options) + # :builder, etc, shouldn't be added to options + assert_equal({ skip_instruct: true }, options) + end +end diff --git a/activesupport/test/core_ext/array/extract_options_test.rb b/activesupport/test/core_ext/array/extract_options_test.rb new file mode 100644 index 0000000000..0481a507cf --- /dev/null +++ b/activesupport/test/core_ext/array/extract_options_test.rb @@ -0,0 +1,45 @@ +require 'abstract_unit' +require 'active_support/core_ext/array' +require 'active_support/core_ext/hash' + +class ExtractOptionsTest < ActiveSupport::TestCase + class HashSubclass < Hash + end + + class ExtractableHashSubclass < Hash + def extractable_options? + true + end + end + + def test_extract_options + assert_equal({}, [].extract_options!) + assert_equal({}, [1].extract_options!) + assert_equal({ a: :b }, [{ a: :b }].extract_options!) + assert_equal({ a: :b }, [1, { a: :b }].extract_options!) + end + + def test_extract_options_doesnt_extract_hash_subclasses + hash = HashSubclass.new + hash[:foo] = 1 + array = [hash] + options = array.extract_options! + assert_equal({}, options) + assert_equal([hash], array) + end + + def test_extract_options_extracts_extractable_subclass + hash = ExtractableHashSubclass.new + hash[:foo] = 1 + array = [hash] + options = array.extract_options! + assert_equal({ foo: 1 }, options) + assert_equal([], array) + end + + def test_extract_options_extracts_hash_with_indifferent_access + array = [{ foo: 1 }.with_indifferent_access] + options = array.extract_options! + assert_equal(1, options[:foo]) + end +end diff --git a/activesupport/test/core_ext/array/grouping_test.rb b/activesupport/test/core_ext/array/grouping_test.rb new file mode 100644 index 0000000000..2eb0f05141 --- /dev/null +++ b/activesupport/test/core_ext/array/grouping_test.rb @@ -0,0 +1,126 @@ +require 'abstract_unit' +require 'active_support/core_ext/array' + +class GroupingTest < ActiveSupport::TestCase + def setup + Fixnum.send :private, :/ # test we avoid Integer#/ (redefined by mathn) + end + + def teardown + Fixnum.send :public, :/ + end + + def test_in_groups_of_with_perfect_fit + groups = [] + ('a'..'i').to_a.in_groups_of(3) do |group| + groups << group + end + + assert_equal [%w(a b c), %w(d e f), %w(g h i)], groups + assert_equal [%w(a b c), %w(d e f), %w(g h i)], ('a'..'i').to_a.in_groups_of(3) + end + + def test_in_groups_of_with_padding + groups = [] + ('a'..'g').to_a.in_groups_of(3) do |group| + groups << group + end + + assert_equal [%w(a b c), %w(d e f), ['g', nil, nil]], groups + end + + def test_in_groups_of_pads_with_specified_values + groups = [] + + ('a'..'g').to_a.in_groups_of(3, 'foo') do |group| + groups << group + end + + assert_equal [%w(a b c), %w(d e f), %w(g foo foo)], groups + end + + def test_in_groups_of_without_padding + groups = [] + + ('a'..'g').to_a.in_groups_of(3, false) do |group| + groups << group + end + + assert_equal [%w(a b c), %w(d e f), %w(g)], groups + end + + def test_in_groups_returned_array_size + array = (1..7).to_a + + 1.upto(array.size + 1) do |number| + assert_equal number, array.in_groups(number).size + end + end + + def test_in_groups_with_empty_array + assert_equal [[], [], []], [].in_groups(3) + end + + def test_in_groups_with_block + array = (1..9).to_a + groups = [] + + array.in_groups(3) do |group| + groups << group + end + + assert_equal array.in_groups(3), groups + end + + def test_in_groups_with_perfect_fit + assert_equal [[1, 2, 3], [4, 5, 6], [7, 8, 9]], + (1..9).to_a.in_groups(3) + end + + def test_in_groups_with_padding + array = (1..7).to_a + + assert_equal [[1, 2, 3], [4, 5, nil], [6, 7, nil]], + array.in_groups(3) + assert_equal [[1, 2, 3], [4, 5, 'foo'], [6, 7, 'foo']], + array.in_groups(3, 'foo') + end + + def test_in_groups_without_padding + assert_equal [[1, 2, 3], [4, 5], [6, 7]], + (1..7).to_a.in_groups(3, false) + end + + def test_in_groups_invalid_argument + assert_raises(ArgumentError) { [].in_groups_of(0) } + assert_raises(ArgumentError) { [].in_groups_of(-1) } + assert_raises(ArgumentError) { [].in_groups_of(nil) } + end +end + +class SplitTest < ActiveSupport::TestCase + def test_split_with_empty_array + assert_equal [[]], [].split(0) + end + + def test_split_with_argument + a = [1, 2, 3, 4, 5] + assert_equal [[1, 2], [4, 5]], a.split(3) + assert_equal [[1, 2, 3, 4, 5]], a.split(0) + assert_equal [1, 2, 3, 4, 5], a + end + + def test_split_with_block + a = (1..10).to_a + assert_equal [[1, 2], [4, 5], [7, 8], [10]], a.split { |i| i % 3 == 0 } + assert_equal [1, 2, 3, 4, 5, 6, 7, 8, 9 ,10], a + end + + def test_split_with_edge_values + a = [1, 2, 3, 4, 5] + assert_equal [[], [2, 3, 4, 5]], a.split(1) + assert_equal [[1, 2, 3, 4], []], a.split(5) + assert_equal [[], [2, 3, 4], []], a.split { |i| i == 1 || i == 5 } + assert_equal [1, 2, 3, 4, 5], a + end +end diff --git a/activesupport/test/core_ext/array/prepend_append_test.rb b/activesupport/test/core_ext/array/prepend_append_test.rb new file mode 100644 index 0000000000..762aa69b2b --- /dev/null +++ b/activesupport/test/core_ext/array/prepend_append_test.rb @@ -0,0 +1,12 @@ +require 'abstract_unit' +require 'active_support/core_ext/array' + +class PrependAppendTest < ActiveSupport::TestCase + def test_append + assert_equal [1, 2], [1].append(2) + end + + def test_prepend + assert_equal [2, 1], [1].prepend(2) + end +end diff --git a/activesupport/test/core_ext/array/wrap_test.rb b/activesupport/test/core_ext/array/wrap_test.rb new file mode 100644 index 0000000000..baf426506f --- /dev/null +++ b/activesupport/test/core_ext/array/wrap_test.rb @@ -0,0 +1,77 @@ +require 'abstract_unit' +require 'active_support/core_ext/array' + +class WrapTest < ActiveSupport::TestCase + class FakeCollection + def to_ary + ["foo", "bar"] + end + end + + class Proxy + def initialize(target) @target = target end + def method_missing(*a) @target.send(*a) end + end + + class DoubtfulToAry + def to_ary + :not_an_array + end + end + + class NilToAry + def to_ary + nil + end + end + + def test_array + ary = %w(foo bar) + assert_same ary, Array.wrap(ary) + end + + def test_nil + assert_equal [], Array.wrap(nil) + end + + def test_object + o = Object.new + assert_equal [o], Array.wrap(o) + end + + def test_string + assert_equal ["foo"], Array.wrap("foo") + end + + def test_string_with_newline + assert_equal ["foo\nbar"], Array.wrap("foo\nbar") + end + + def test_object_with_to_ary + assert_equal ["foo", "bar"], Array.wrap(FakeCollection.new) + end + + def test_proxy_object + p = Proxy.new(Object.new) + assert_equal [p], Array.wrap(p) + end + + def test_proxy_to_object_with_to_ary + p = Proxy.new(FakeCollection.new) + assert_equal [p], Array.wrap(p) + end + + def test_struct + o = Struct.new(:foo).new(123) + assert_equal [o], Array.wrap(o) + end + + def test_wrap_returns_wrapped_if_to_ary_returns_nil + o = NilToAry.new + assert_equal [o], Array.wrap(o) + end + + def test_wrap_does_not_complain_if_to_ary_does_not_return_an_array + assert_equal DoubtfulToAry.new.to_ary, Array.wrap(DoubtfulToAry.new) + end +end diff --git a/activesupport/test/core_ext/array_ext_test.rb b/activesupport/test/core_ext/array_ext_test.rb deleted file mode 100644 index 57722fd52a..0000000000 --- a/activesupport/test/core_ext/array_ext_test.rb +++ /dev/null @@ -1,451 +0,0 @@ -require 'abstract_unit' -require 'active_support/core_ext/array' -require 'active_support/core_ext/big_decimal' -require 'active_support/core_ext/object/conversions' - -require 'active_support/core_ext' # FIXME: pulling in all to_xml extensions -require 'active_support/hash_with_indifferent_access' - -class ArrayExtAccessTests < ActiveSupport::TestCase - def test_from - assert_equal %w( a b c d ), %w( a b c d ).from(0) - assert_equal %w( c d ), %w( a b c d ).from(2) - assert_equal %w(), %w( a b c d ).from(10) - end - - def test_to - assert_equal %w( a ), %w( a b c d ).to(0) - assert_equal %w( a b c ), %w( a b c d ).to(2) - assert_equal %w( a b c d ), %w( a b c d ).to(10) - end - - def test_second_through_tenth - array = (1..42).to_a - - assert_equal array[1], array.second - assert_equal array[2], array.third - assert_equal array[3], array.fourth - assert_equal array[4], array.fifth - assert_equal array[41], array.forty_two - end -end - -class ArrayExtToParamTests < ActiveSupport::TestCase - class ToParam < String - def to_param - "#{self}1" - end - end - - def test_string_array - assert_equal '', %w().to_param - assert_equal 'hello/world', %w(hello world).to_param - assert_equal 'hello/10', %w(hello 10).to_param - end - - def test_number_array - assert_equal '10/20', [10, 20].to_param - end - - def test_to_param_array - assert_equal 'custom1/param1', [ToParam.new('custom'), ToParam.new('param')].to_param - end -end - -class ArrayExtToSentenceTests < ActiveSupport::TestCase - def test_plain_array_to_sentence - assert_equal "", [].to_sentence - assert_equal "one", ['one'].to_sentence - assert_equal "one and two", ['one', 'two'].to_sentence - assert_equal "one, two, and three", ['one', 'two', 'three'].to_sentence - end - - def test_to_sentence_with_words_connector - assert_equal "one two, and three", ['one', 'two', 'three'].to_sentence(:words_connector => ' ') - assert_equal "one & two, and three", ['one', 'two', 'three'].to_sentence(:words_connector => ' & ') - assert_equal "onetwo, and three", ['one', 'two', 'three'].to_sentence(:words_connector => nil) - end - - def test_to_sentence_with_last_word_connector - assert_equal "one, two, and also three", ['one', 'two', 'three'].to_sentence(:last_word_connector => ', and also ') - assert_equal "one, twothree", ['one', 'two', 'three'].to_sentence(:last_word_connector => nil) - assert_equal "one, two three", ['one', 'two', 'three'].to_sentence(:last_word_connector => ' ') - assert_equal "one, two and three", ['one', 'two', 'three'].to_sentence(:last_word_connector => ' and ') - end - - def test_two_elements - assert_equal "one and two", ['one', 'two'].to_sentence - assert_equal "one two", ['one', 'two'].to_sentence(:two_words_connector => ' ') - end - - def test_one_element - assert_equal "one", ['one'].to_sentence - end - - def test_one_element_not_same_object - elements = ["one"] - assert_not_equal elements[0].object_id, elements.to_sentence.object_id - end - - def test_one_non_string_element - assert_equal '1', [1].to_sentence - end - - def test_does_not_modify_given_hash - options = { words_connector: ' ' } - 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 - def test_to_s_db - collection = [ - Class.new { def id() 1 end }.new, - Class.new { def id() 2 end }.new, - Class.new { def id() 3 end }.new - ] - - assert_equal "null", [].to_s(:db) - assert_equal "1,2,3", collection.to_s(:db) - end -end - -class ArrayExtGroupingTests < ActiveSupport::TestCase - def setup - Fixnum.send :private, :/ # test we avoid Integer#/ (redefined by mathn) - end - - def teardown - Fixnum.send :public, :/ - end - - def test_in_groups_of_with_perfect_fit - groups = [] - ('a'..'i').to_a.in_groups_of(3) do |group| - groups << group - end - - assert_equal [%w(a b c), %w(d e f), %w(g h i)], groups - assert_equal [%w(a b c), %w(d e f), %w(g h i)], ('a'..'i').to_a.in_groups_of(3) - end - - def test_in_groups_of_with_padding - groups = [] - ('a'..'g').to_a.in_groups_of(3) do |group| - groups << group - end - - assert_equal [%w(a b c), %w(d e f), ['g', nil, nil]], groups - end - - def test_in_groups_of_pads_with_specified_values - groups = [] - - ('a'..'g').to_a.in_groups_of(3, 'foo') do |group| - groups << group - end - - assert_equal [%w(a b c), %w(d e f), ['g', 'foo', 'foo']], groups - end - - def test_in_groups_of_without_padding - groups = [] - - ('a'..'g').to_a.in_groups_of(3, false) do |group| - groups << group - end - - assert_equal [%w(a b c), %w(d e f), ['g']], groups - end - - def test_in_groups_returned_array_size - array = (1..7).to_a - - 1.upto(array.size + 1) do |number| - assert_equal number, array.in_groups(number).size - end - end - - def test_in_groups_with_empty_array - assert_equal [[], [], []], [].in_groups(3) - end - - def test_in_groups_with_block - array = (1..9).to_a - groups = [] - - array.in_groups(3) do |group| - groups << group - end - - assert_equal array.in_groups(3), groups - end - - def test_in_groups_with_perfect_fit - assert_equal [[1, 2, 3], [4, 5, 6], [7, 8, 9]], - (1..9).to_a.in_groups(3) - end - - def test_in_groups_with_padding - array = (1..7).to_a - - assert_equal [[1, 2, 3], [4, 5, nil], [6, 7, nil]], - array.in_groups(3) - assert_equal [[1, 2, 3], [4, 5, 'foo'], [6, 7, 'foo']], - array.in_groups(3, 'foo') - end - - def test_in_groups_without_padding - assert_equal [[1, 2, 3], [4, 5], [6, 7]], - (1..7).to_a.in_groups(3, false) - end -end - -class ArraySplitTests < ActiveSupport::TestCase - def test_split_with_empty_array - assert_equal [[]], [].split(0) - end - - def test_split_with_argument - a = [1, 2, 3, 4, 5] - assert_equal [[1, 2], [4, 5]], a.split(3) - assert_equal [[1, 2, 3, 4, 5]], a.split(0) - assert_equal [1, 2, 3, 4, 5], a - end - - def test_split_with_block - a = (1..10).to_a - assert_equal [[1, 2], [4, 5], [7, 8], [10]], a.split { |i| i % 3 == 0 } - assert_equal [1, 2, 3, 4, 5, 6, 7, 8, 9 ,10], a - end - - def test_split_with_edge_values - a = [1, 2, 3, 4, 5] - assert_equal [[], [2, 3, 4, 5]], a.split(1) - assert_equal [[1, 2, 3, 4], []], a.split(5) - assert_equal [[], [2, 3, 4], []], a.split { |i| i == 1 || i == 5 } - assert_equal [1, 2, 3, 4, 5], a - end -end - -class ArrayToXmlTests < ActiveSupport::TestCase - def test_to_xml - xml = [ - { :name => "David", :age => 26, :age_in_millis => 820497600000 }, - { :name => "Jason", :age => 31, :age_in_millis => BigDecimal.new('1.0') } - ].to_xml(:skip_instruct => true, :indent => 0) - - assert_equal '<objects type="array"><object>', xml.first(30) - assert xml.include?(%(<age type="integer">26</age>)), xml - assert xml.include?(%(<age-in-millis type="integer">820497600000</age-in-millis>)), xml - assert xml.include?(%(<name>David</name>)), xml - assert xml.include?(%(<age type="integer">31</age>)), xml - assert xml.include?(%(<age-in-millis type="decimal">1.0</age-in-millis>)), xml - assert xml.include?(%(<name>Jason</name>)), xml - end - - def test_to_xml_with_dedicated_name - xml = [ - { :name => "David", :age => 26, :age_in_millis => 820497600000 }, { :name => "Jason", :age => 31 } - ].to_xml(:skip_instruct => true, :indent => 0, :root => "people") - - assert_equal '<people type="array"><person>', xml.first(29) - end - - def test_to_xml_with_options - xml = [ - { :name => "David", :street_address => "Paulina" }, { :name => "Jason", :street_address => "Evergreen" } - ].to_xml(:skip_instruct => true, :skip_types => true, :indent => 0) - - assert_equal "<objects><object>", xml.first(17) - assert xml.include?(%(<street-address>Paulina</street-address>)) - assert xml.include?(%(<name>David</name>)) - assert xml.include?(%(<street-address>Evergreen</street-address>)) - assert xml.include?(%(<name>Jason</name>)) - end - - def test_to_xml_with_dasherize_false - xml = [ - { :name => "David", :street_address => "Paulina" }, { :name => "Jason", :street_address => "Evergreen" } - ].to_xml(:skip_instruct => true, :skip_types => true, :indent => 0, :dasherize => false) - - assert_equal "<objects><object>", xml.first(17) - assert xml.include?(%(<street_address>Paulina</street_address>)) - assert xml.include?(%(<street_address>Evergreen</street_address>)) - end - - def test_to_xml_with_dasherize_true - xml = [ - { :name => "David", :street_address => "Paulina" }, { :name => "Jason", :street_address => "Evergreen" } - ].to_xml(:skip_instruct => true, :skip_types => true, :indent => 0, :dasherize => true) - - assert_equal "<objects><object>", xml.first(17) - assert xml.include?(%(<street-address>Paulina</street-address>)) - assert xml.include?(%(<street-address>Evergreen</street-address>)) - end - - def test_to_with_instruct - xml = [ - { :name => "David", :age => 26, :age_in_millis => 820497600000 }, - { :name => "Jason", :age => 31, :age_in_millis => BigDecimal.new('1.0') } - ].to_xml(:skip_instruct => false, :indent => 0) - - assert_match(/^<\?xml [^>]*/, xml) - assert_equal 0, xml.rindex(/<\?xml /) - end - - def test_to_xml_with_block - xml = [ - { :name => "David", :age => 26, :age_in_millis => 820497600000 }, - { :name => "Jason", :age => 31, :age_in_millis => BigDecimal.new('1.0') } - ].to_xml(:skip_instruct => true, :indent => 0) do |builder| - builder.count 2 - end - - assert xml.include?(%(<count>2</count>)), xml - end - - def test_to_xml_with_empty - xml = [].to_xml - assert_match(/type="array"\/>/, xml) - end - - def test_to_xml_dups_options - options = {:skip_instruct => true} - [].to_xml(options) - # :builder, etc, shouldn't be added to options - assert_equal({:skip_instruct => true}, options) - end -end - -class ArrayExtractOptionsTests < ActiveSupport::TestCase - class HashSubclass < Hash - end - - class ExtractableHashSubclass < Hash - def extractable_options? - true - end - end - - def test_extract_options - assert_equal({}, [].extract_options!) - assert_equal({}, [1].extract_options!) - assert_equal({:a=>:b}, [{:a=>:b}].extract_options!) - assert_equal({:a=>:b}, [1, {:a=>:b}].extract_options!) - end - - def test_extract_options_doesnt_extract_hash_subclasses - hash = HashSubclass.new - hash[:foo] = 1 - array = [hash] - options = array.extract_options! - assert_equal({}, options) - assert_equal [hash], array - end - - def test_extract_options_extracts_extractable_subclass - hash = ExtractableHashSubclass.new - hash[:foo] = 1 - array = [hash] - options = array.extract_options! - assert_equal({:foo => 1}, options) - assert_equal [], array - end - - def test_extract_options_extracts_hwia - hash = [{:foo => 1}.with_indifferent_access] - options = hash.extract_options! - assert_equal 1, options[:foo] - end -end - -class ArrayWrapperTests < ActiveSupport::TestCase - class FakeCollection - def to_ary - ["foo", "bar"] - end - end - - class Proxy - def initialize(target) @target = target end - def method_missing(*a) @target.send(*a) end - end - - class DoubtfulToAry - def to_ary - :not_an_array - end - end - - class NilToAry - def to_ary - nil - end - end - - def test_array - ary = %w(foo bar) - assert_same ary, Array.wrap(ary) - end - - def test_nil - assert_equal [], Array.wrap(nil) - end - - def test_object - o = Object.new - assert_equal [o], Array.wrap(o) - end - - def test_string - assert_equal ["foo"], Array.wrap("foo") - end - - def test_string_with_newline - assert_equal ["foo\nbar"], Array.wrap("foo\nbar") - end - - def test_object_with_to_ary - assert_equal ["foo", "bar"], Array.wrap(FakeCollection.new) - end - - def test_proxy_object - p = Proxy.new(Object.new) - assert_equal [p], Array.wrap(p) - end - - def test_proxy_to_object_with_to_ary - p = Proxy.new(FakeCollection.new) - assert_equal [p], Array.wrap(p) - end - - def test_struct - o = Struct.new(:foo).new(123) - assert_equal [o], Array.wrap(o) - end - - def test_wrap_returns_wrapped_if_to_ary_returns_nil - o = NilToAry.new - assert_equal [o], Array.wrap(o) - end - - def test_wrap_does_not_complain_if_to_ary_does_not_return_an_array - assert_equal DoubtfulToAry.new.to_ary, Array.wrap(DoubtfulToAry.new) - end -end - -class ArrayPrependAppendTest < ActiveSupport::TestCase - def test_append - assert_equal [1, 2], [1].append(2) - end - - def test_prepend - assert_equal [2, 1], [1].prepend(2) - end -end diff --git a/activesupport/test/core_ext/bigdecimal_test.rb b/activesupport/test/core_ext/bigdecimal_test.rb index b386e55d6c..423a3f2e9d 100644 --- a/activesupport/test/core_ext/bigdecimal_test.rb +++ b/activesupport/test/core_ext/bigdecimal_test.rb @@ -2,18 +2,6 @@ require 'abstract_unit' require 'active_support/core_ext/big_decimal' class BigDecimalTest < ActiveSupport::TestCase - def test_to_yaml - assert_match("--- 100000.30020320320000000000000000000000000000001\n", BigDecimal.new('100000.30020320320000000000000000000000000000001').to_yaml) - assert_match("--- .Inf\n", BigDecimal.new('Infinity').to_yaml) - assert_match("--- .NaN\n", BigDecimal.new('NaN').to_yaml) - assert_match("--- -.Inf\n", BigDecimal.new('-Infinity').to_yaml) - end - - def test_to_d - bd = BigDecimal.new '10' - assert_equal bd, bd.to_d - end - def test_to_s bd = BigDecimal.new '0.01' assert_equal '0.01', bd.to_s diff --git a/activesupport/test/core_ext/class/delegating_attributes_test.rb b/activesupport/test/core_ext/class/delegating_attributes_test.rb index 0e0742d147..447b1d10ad 100644 --- a/activesupport/test/core_ext/class/delegating_attributes_test.rb +++ b/activesupport/test/core_ext/class/delegating_attributes_test.rb @@ -6,14 +6,18 @@ module DelegatingFixtures end class Child < Parent - superclass_delegating_accessor :some_attribute + ActiveSupport::Deprecation.silence do + superclass_delegating_accessor :some_attribute + end end class Mokopuna < Child end class PercysMom - superclass_delegating_accessor :superpower + ActiveSupport::Deprecation.silence do + superclass_delegating_accessor :superpower + end end class Percy < PercysMom @@ -29,7 +33,10 @@ class DelegatingAttributesTest < ActiveSupport::TestCase end def test_simple_accessor_declaration - single_class.superclass_delegating_accessor :both + assert_deprecated do + single_class.superclass_delegating_accessor :both + end + # Class should have accessor and mutator # the instance should have an accessor only assert_respond_to single_class, :both @@ -40,7 +47,11 @@ class DelegatingAttributesTest < ActiveSupport::TestCase def test_simple_accessor_declaration_with_instance_reader_false _instance_methods = single_class.public_instance_methods - single_class.superclass_delegating_accessor :no_instance_reader, :instance_reader => false + + assert_deprecated do + single_class.superclass_delegating_accessor :no_instance_reader, :instance_reader => false + end + assert_respond_to single_class, :no_instance_reader assert_respond_to single_class, :no_instance_reader= assert !_instance_methods.include?(:no_instance_reader) @@ -49,7 +60,9 @@ class DelegatingAttributesTest < ActiveSupport::TestCase end def test_working_with_simple_attributes - single_class.superclass_delegating_accessor :both + assert_deprecated do + single_class.superclass_delegating_accessor :both + end single_class.both = "HMMM" @@ -65,7 +78,11 @@ class DelegatingAttributesTest < ActiveSupport::TestCase def test_child_class_delegates_to_parent_but_can_be_overridden parent = Class.new - parent.superclass_delegating_accessor :both + + assert_deprecated do + parent.superclass_delegating_accessor :both + end + child = Class.new(parent) parent.both = "1" assert_equal "1", child.both @@ -97,4 +114,9 @@ class DelegatingAttributesTest < ActiveSupport::TestCase Child.some_attribute=nil end + def test_deprecation_warning + assert_deprecated(/superclass_delegating_accessor is deprecated/) do + single_class.superclass_delegating_accessor :test_attribute + end + end end diff --git a/activesupport/test/core_ext/date_and_time_behavior.rb b/activesupport/test/core_ext/date_and_time_behavior.rb index b4ef5a0597..784547bdf8 100644 --- a/activesupport/test/core_ext/date_and_time_behavior.rb +++ b/activesupport/test/core_ext/date_and_time_behavior.rb @@ -6,11 +6,21 @@ module DateAndTimeBehavior assert_equal date_time_init(2005,2,28,10,10,10), date_time_init(2005,3,2,10,10,10).yesterday.yesterday end + def test_prev_day + assert_equal date_time_init(2005,2,21,10,10,10), date_time_init(2005,2,22,10,10,10).prev_day + assert_equal date_time_init(2005,2,28,10,10,10), date_time_init(2005,3,2,10,10,10).prev_day.prev_day + end + def test_tomorrow assert_equal date_time_init(2005,2,23,10,10,10), date_time_init(2005,2,22,10,10,10).tomorrow assert_equal date_time_init(2005,3,2,10,10,10), date_time_init(2005,2,28,10,10,10).tomorrow.tomorrow end + def test_next_day + assert_equal date_time_init(2005,2,23,10,10,10), date_time_init(2005,2,22,10,10,10).next_day + assert_equal date_time_init(2005,3,2,10,10,10), date_time_init(2005,2,28,10,10,10).next_day.next_day + end + def test_days_ago assert_equal date_time_init(2005,6,4,10,10,10), date_time_init(2005,6,5,10,10,10).days_ago(1) assert_equal date_time_init(2005,5,31,10,10,10), date_time_init(2005,6,5,10,10,10).days_ago(5) @@ -115,6 +125,28 @@ module DateAndTimeBehavior end end + def test_next_week_at_same_time + assert_equal date_time_init(2005,2,28,15,15,10), date_time_init(2005,2,22,15,15,10).next_week(:monday, same_time: true) + assert_equal date_time_init(2005,3,4,15,15,10), date_time_init(2005,2,22,15,15,10).next_week(:friday, same_time: true) + assert_equal date_time_init(2006,10,30,0,0,0), date_time_init(2006,10,23,0,0,0).next_week(:monday, same_time: true) + assert_equal date_time_init(2006,11,1,0,0,0), date_time_init(2006,10,23,0,0,0).next_week(:wednesday, same_time: true) + end + + def test_next_weekday_on_wednesday + assert_equal date_time_init(2015,1,8,0,0,0), date_time_init(2015,1,7,0,0,0).next_weekday + assert_equal date_time_init(2015,1,8,15,15,10), date_time_init(2015,1,7,15,15,10).next_weekday + end + + def test_next_weekday_on_friday + assert_equal date_time_init(2015,1,5,0,0,0), date_time_init(2015,1,2,0,0,0).next_weekday + assert_equal date_time_init(2015,1,5,15,15,10), date_time_init(2015,1,2,15,15,10).next_weekday + end + + def test_next_weekday_on_saturday + assert_equal date_time_init(2015,1,5,0,0,0), date_time_init(2015,1,3,0,0,0).next_weekday + assert_equal date_time_init(2015,1,5,15,15,10), date_time_init(2015,1,3,15,15,10).next_weekday + end + def test_next_month_on_31st assert_equal date_time_init(2005,9,30,15,15,10), date_time_init(2005,8,31,15,15,10).next_month end @@ -144,6 +176,29 @@ module DateAndTimeBehavior end end + def test_prev_week_at_same_time + assert_equal date_time_init(2005,2,21,15,15,10), date_time_init(2005,3,1,15,15,10).prev_week(:monday, same_time: true) + assert_equal date_time_init(2005,2,22,15,15,10), date_time_init(2005,3,1,15,15,10).prev_week(:tuesday, same_time: true) + assert_equal date_time_init(2005,2,25,15,15,10), date_time_init(2005,3,1,15,15,10).prev_week(:friday, same_time: true) + assert_equal date_time_init(2006,10,30,0,0,0), date_time_init(2006,11,6,0,0,0).prev_week(:monday, same_time: true) + assert_equal date_time_init(2006,11,15,0,0,0), date_time_init(2006,11,23,0,0,0).prev_week(:wednesday, same_time: true) + end + + def test_prev_weekday_on_wednesday + assert_equal date_time_init(2015,1,6,0,0,0), date_time_init(2015,1,7,0,0,0).prev_weekday + assert_equal date_time_init(2015,1,6,15,15,10), date_time_init(2015,1,7,15,15,10).prev_weekday + end + + def test_prev_weekday_on_monday + assert_equal date_time_init(2015,1,2,0,0,0), date_time_init(2015,1,5,0,0,0).prev_weekday + assert_equal date_time_init(2015,1,2,15,15,10), date_time_init(2015,1,5,15,15,10).prev_weekday + end + + def test_prev_weekday_on_sunday + assert_equal date_time_init(2015,1,2,0,0,0), date_time_init(2015,1,4,0,0,0).prev_weekday + assert_equal date_time_init(2015,1,2,15,15,10), date_time_init(2015,1,4,15,15,10).prev_weekday + end + def test_prev_month_on_31st assert_equal date_time_init(2004,2,29,10,10,10), date_time_init(2004,3,31,10,10,10).prev_month end @@ -231,6 +286,21 @@ module DateAndTimeBehavior end end + def test_on_weekend_on_saturday + assert date_time_init(2015,1,3,0,0,0).on_weekend? + assert date_time_init(2015,1,3,15,15,10).on_weekend? + end + + def test_on_weekend_on_sunday + assert date_time_init(2015,1,4,0,0,0).on_weekend? + assert date_time_init(2015,1,4,15,15,10).on_weekend? + end + + def test_on_weekend_on_monday + assert_not date_time_init(2015,1,5,0,0,0).on_weekend? + assert_not date_time_init(2015,1,5,15,15,10).on_weekend? + end + def with_bw_default(bw = :monday) old_bw = Date.beginning_of_week Date.beginning_of_week = bw diff --git a/activesupport/test/core_ext/date_ext_test.rb b/activesupport/test/core_ext/date_ext_test.rb index 5d0af035cc..e89be25b53 100644 --- a/activesupport/test/core_ext/date_ext_test.rb +++ b/activesupport/test/core_ext/date_ext_test.rb @@ -1,6 +1,7 @@ require 'abstract_unit' require 'active_support/time' require 'core_ext/date_and_time_behavior' +require 'time_zone_test_helpers' class DateExtCalculationsTest < ActiveSupport::TestCase def date_time_init(year,month,day,*args) @@ -8,6 +9,7 @@ class DateExtCalculationsTest < ActiveSupport::TestCase end include DateAndTimeBehavior + include TimeZoneTestHelpers def test_yesterday_in_calendar_reform assert_equal Date.new(1582,10,4), Date.new(1582,10,15).yesterday @@ -349,22 +351,6 @@ class DateExtCalculationsTest < ActiveSupport::TestCase Date.new(2005,2,28).advance(options) assert_equal({ :years => 3, :months => 11, :days => 2 }, options) 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 - - def with_tz_default(tz = nil) - old_tz = Time.zone - Time.zone = tz - yield - ensure - Time.zone = old_tz - end end class DateExtBehaviorTest < ActiveSupport::TestCase diff --git a/activesupport/test/core_ext/date_time_ext_test.rb b/activesupport/test/core_ext/date_time_ext_test.rb index 0a40aeb96c..74319ecd09 100644 --- a/activesupport/test/core_ext/date_time_ext_test.rb +++ b/activesupport/test/core_ext/date_time_ext_test.rb @@ -1,6 +1,7 @@ require 'abstract_unit' require 'active_support/time' require 'core_ext/date_and_time_behavior' +require 'time_zone_test_helpers' class DateTimeExtCalculationsTest < ActiveSupport::TestCase def date_time_init(year,month,day,hour,minute,second,*args) @@ -8,6 +9,7 @@ class DateTimeExtCalculationsTest < ActiveSupport::TestCase end include DateAndTimeBehavior + include TimeZoneTestHelpers def test_to_s datetime = DateTime.new(2005, 2, 21, 14, 30, 0, 0) @@ -162,6 +164,12 @@ class DateTimeExtCalculationsTest < ActiveSupport::TestCase assert_equal DateTime.civil(2013,10,17,20,22,19), DateTime.civil(2005,2,28,15,15,10).advance(:years => 7, :months => 19, :weeks => 2, :days => 5, :hours => 5, :minutes => 7, :seconds => 9) end + def test_advance_partial_days + assert_equal DateTime.civil(2012,9,29,13,15,10), DateTime.civil(2012,9,28,1,15,10).advance(:days => 1.5) + assert_equal DateTime.civil(2012,9,28,13,15,10), DateTime.civil(2012,9,28,1,15,10).advance(:days => 0.5) + assert_equal DateTime.civil(2012,10,29,13,15,10), DateTime.civil(2012,9,28,1,15,10).advance(:days => 1.5, :months => 1) + end + def test_advanced_processes_first_the_date_deltas_and_then_the_time_deltas # If the time deltas were processed first, the following datetimes would be advanced to 2010/04/01 instead. assert_equal DateTime.civil(2010, 3, 29), DateTime.civil(2010, 2, 28, 23, 59, 59).advance(:months => 1, :seconds => 1) @@ -330,6 +338,7 @@ class DateTimeExtCalculationsTest < ActiveSupport::TestCase def test_to_f assert_equal 946684800.0, DateTime.civil(2000).to_f assert_equal 946684800.0, DateTime.civil(1999,12,31,19,0,0,Rational(-5,24)).to_f + assert_equal 946684800.5, DateTime.civil(1999,12,31,19,0,0.5,Rational(-5,24)).to_f end def test_to_i @@ -346,12 +355,4 @@ class DateTimeExtCalculationsTest < ActiveSupport::TestCase 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 - yield - ensure - old_tz ? ENV['TZ'] = old_tz : ENV.delete('TZ') - end end diff --git a/activesupport/test/core_ext/digest/uuid_test.rb b/activesupport/test/core_ext/digest/uuid_test.rb new file mode 100644 index 0000000000..08e0a1d6e1 --- /dev/null +++ b/activesupport/test/core_ext/digest/uuid_test.rb @@ -0,0 +1,24 @@ +require 'abstract_unit' +require 'active_support/core_ext/digest/uuid' + +class DigestUUIDExt < ActiveSupport::TestCase + def test_v3_uuids + assert_equal "3d813cbb-47fb-32ba-91df-831e1593ac29", Digest::UUID.uuid_v3(Digest::UUID::DNS_NAMESPACE, "www.widgets.com") + assert_equal "86df55fb-428e-3843-8583-ba3c05f290bc", Digest::UUID.uuid_v3(Digest::UUID::URL_NAMESPACE, "http://www.widgets.com") + assert_equal "8c29ab0e-a2dc-3482-b5eb-20cb2e2387a1", Digest::UUID.uuid_v3(Digest::UUID::OID_NAMESPACE, "1.2.3") + assert_equal "ee49149d-53a4-304a-890b-468229f6afc3", Digest::UUID.uuid_v3(Digest::UUID::X500_NAMESPACE, "cn=John Doe, ou=People, o=Acme, Inc., c=US") + end + + def test_v5_uuids + assert_equal "21f7f8de-8051-5b89-8680-0195ef798b6a", Digest::UUID.uuid_v5(Digest::UUID::DNS_NAMESPACE, "www.widgets.com") + assert_equal "4e570fd8-186d-5a74-90f0-4d28e34673a1", Digest::UUID.uuid_v5(Digest::UUID::URL_NAMESPACE, "http://www.widgets.com") + assert_equal "42d5e23b-3a02-5135-85c6-52d1102f1f00", Digest::UUID.uuid_v5(Digest::UUID::OID_NAMESPACE, "1.2.3") + assert_equal "fd5b2ddf-bcfe-58b6-90d6-db50f74db527", Digest::UUID.uuid_v5(Digest::UUID::X500_NAMESPACE, "cn=John Doe, ou=People, o=Acme, Inc., c=US") + end + + def test_invalid_hash_class + assert_raise ArgumentError do + Digest::UUID.uuid_from_hash(Digest::SHA2, Digest::UUID::OID_NAMESPACE, '1.2.3') + end + end +end diff --git a/activesupport/test/core_ext/duplicable_test.rb b/activesupport/test/core_ext/duplicable_test.rb deleted file mode 100644 index e0566e012c..0000000000 --- a/activesupport/test/core_ext/duplicable_test.rb +++ /dev/null @@ -1,38 +0,0 @@ -require 'abstract_unit' -require 'bigdecimal' -require 'active_support/core_ext/object/duplicable' -require 'active_support/core_ext/numeric/time' - -class DuplicableTest < ActiveSupport::TestCase - RAISE_DUP = [nil, false, true, :symbol, 1, 2.3, 5.seconds] - YES = ['1', Object.new, /foo/, [], {}, Time.now, Class.new, Module.new] - NO = [] - - begin - bd = BigDecimal.new('4.56') - YES << bd.dup - rescue TypeError - RAISE_DUP << bd - end - - - def test_duplicable - (RAISE_DUP + NO).each do |v| - assert !v.duplicable? - end - - YES.each do |v| - assert v.duplicable?, "#{v.class} should be duplicable" - end - - (YES + NO).each do |v| - assert_nothing_raised { v.dup } - end - - RAISE_DUP.each do |v| - assert_raises(TypeError, v.class.name) do - v.dup - end - end - end -end diff --git a/activesupport/test/core_ext/duration_test.rb b/activesupport/test/core_ext/duration_test.rb index 28ba33331e..c283b546e6 100644 --- a/activesupport/test/core_ext/duration_test.rb +++ b/activesupport/test/core_ext/duration_test.rb @@ -2,8 +2,11 @@ require 'abstract_unit' require 'active_support/inflector' require 'active_support/time' require 'active_support/json' +require 'time_zone_test_helpers' class DurationTest < ActiveSupport::TestCase + include TimeZoneTestHelpers + def test_is_a d = 1.day assert d.is_a?(ActiveSupport::Duration) @@ -17,11 +20,16 @@ class DurationTest < ActiveSupport::TestCase assert !d.is_a?(k) end + def test_instance_of + assert 1.minute.instance_of?(Fixnum) + assert 2.days.instance_of?(ActiveSupport::Duration) + assert !3.second.instance_of?(Numeric) + end + def test_threequals assert ActiveSupport::Duration === 1.day assert !(ActiveSupport::Duration === 1.day.to_i) assert !(ActiveSupport::Duration === 'foo') - assert !(ActiveSupport::Duration === ActiveSupport::ProxyObject.new) end def test_equals @@ -31,6 +39,24 @@ class DurationTest < ActiveSupport::TestCase assert !(1.day == 'foo') end + def test_to_s + assert_equal "1", 1.second.to_s + end + + def test_eql + rubinius_skip "Rubinius' #eql? definition relies on #instance_of? " \ + "which behaves oddly for the sake of backward-compatibility." + + assert 1.minute.eql?(1.minute) + assert 1.minute.eql?(60.seconds) + assert 2.days.eql?(48.hours) + assert !1.second.eql?(1) + assert !1.eql?(1.second) + assert 1.minute.eql?(180.seconds - 2.minutes) + assert !1.minute.eql?(60) + assert !1.minute.eql?('foo') + end + def test_inspect assert_equal '0 seconds', 0.seconds.inspect assert_equal '1 month', 1.month.inspect @@ -44,6 +70,15 @@ class DurationTest < ActiveSupport::TestCase assert_equal '14 days', 1.fortnight.inspect end + def test_inspect_locale + current_locale = I18n.default_locale + I18n.default_locale = :de + I18n.backend.store_translations(:de, { support: { array: { last_word_connector: ' und ' } } }) + assert_equal '10 years, 1 month und 1 day', (10.years + 1.month + 1.day).inspect + ensure + I18n.default_locale = current_locale + end + def test_minus_with_duration_does_not_break_subtraction_of_date_from_date assert_nothing_raised { Date.today - Date.today } end @@ -71,8 +106,8 @@ class DurationTest < ActiveSupport::TestCase def test_since_and_ago t = Time.local(2000) - assert t + 1, 1.second.since(t) - assert t - 1, 1.second.ago(t) + assert_equal t + 1, 1.second.since(t) + assert_equal t - 1, 1.second.ago(t) end def test_since_and_ago_without_argument @@ -152,6 +187,10 @@ class DurationTest < ActiveSupport::TestCase assert_equal counter, 60 end + def test_as_json + assert_equal 172800, 2.days.as_json + end + def test_to_json assert_equal '172800', 2.days.to_json end @@ -161,11 +200,24 @@ class DurationTest < ActiveSupport::TestCase assert_equal cased, "ok" 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 + def test_respond_to + assert_respond_to 1.day, :since + assert_respond_to 1.day, :zero? + end + + def test_hash + assert_equal 1.minute.hash, 60.seconds.hash + end + + def test_comparable + assert_equal(-1, (0.seconds <=> 1.second)) + assert_equal(-1, (1.second <=> 1.minute)) + assert_equal(-1, (1 <=> 1.minute)) + assert_equal(0, (0.seconds <=> 0.seconds)) + assert_equal(0, (0.seconds <=> 0.minutes)) + assert_equal(0, (1.second <=> 1.second)) + assert_equal(1, (1.second <=> 0.second)) + assert_equal(1, (1.minute <=> 1.second)) + assert_equal(1, (61 <=> 1.minute)) + end end diff --git a/activesupport/test/core_ext/enumerable_test.rb b/activesupport/test/core_ext/enumerable_test.rb index 6781e3c20e..346dc3d208 100644 --- a/activesupport/test/core_ext/enumerable_test.rb +++ b/activesupport/test/core_ext/enumerable_test.rb @@ -8,7 +8,6 @@ class SummablePayment < Payment end class EnumerableTests < ActiveSupport::TestCase - Enumerator = [].each.class class GenericEnumerable include Enumerable @@ -21,26 +20,6 @@ class EnumerableTests < ActiveSupport::TestCase end end - def test_group_by - names = %w(marcel sam david jeremy) - klass = Struct.new(:name) - objects = (1..50).map do - klass.new names.sample - end - - enum = GenericEnumerable.new(objects) - grouped = enum.group_by { |object| object.name } - - grouped.each do |name, group| - assert group.all? { |person| person.name == name } - end - - assert_equal objects.uniq.map(&:name), grouped.keys - assert({}.merge(grouped), "Could not convert ActiveSupport::OrderedHash into Hash") - assert_equal Enumerator, enum.group_by.class - assert_equal grouped, enum.group_by.each(&:name) - end - def test_sums enum = GenericEnumerable.new([5, 15, 10]) assert_equal 30, enum.sum @@ -92,10 +71,14 @@ class EnumerableTests < ActiveSupport::TestCase def test_index_by payments = GenericEnumerable.new([ Payment.new(5), Payment.new(15), Payment.new(10) ]) assert_equal({ 5 => Payment.new(5), 15 => Payment.new(15), 10 => Payment.new(10) }, - payments.index_by { |p| p.price }) + payments.index_by(&:price)) assert_equal Enumerator, payments.index_by.class + if Enumerator.method_defined? :size + assert_equal nil, payments.index_by.size + assert_equal 42, (1..42).index_by.size + end assert_equal({ 5 => Payment.new(5), 15 => Payment.new(15), 10 => Payment.new(10) }, - payments.index_by.each { |p| p.price }) + payments.index_by.each(&:price)) end def test_many diff --git a/activesupport/test/core_ext/hash/transform_keys_test.rb b/activesupport/test/core_ext/hash/transform_keys_test.rb new file mode 100644 index 0000000000..a7e12117f3 --- /dev/null +++ b/activesupport/test/core_ext/hash/transform_keys_test.rb @@ -0,0 +1,32 @@ +require 'abstract_unit' +require 'active_support/core_ext/hash/keys' + +class TransformKeysTest < ActiveSupport::TestCase + test "transform_keys returns a new hash with the keys computed from the block" do + original = { a: 'a', b: 'b' } + mapped = original.transform_keys { |k| "#{k}!".to_sym } + + assert_equal({ a: 'a', b: 'b' }, original) + assert_equal({ a!: 'a', b!: 'b' }, mapped) + end + + test "transform_keys! modifies the keys of the original" do + original = { a: 'a', b: 'b' } + mapped = original.transform_keys! { |k| "#{k}!".to_sym } + + assert_equal({ a!: 'a', b!: 'b' }, original) + assert_same original, mapped + end + + test "transform_keys returns an Enumerator if no block is given" do + original = { a: 'a', b: 'b' } + enumerator = original.transform_keys + assert_equal Enumerator, enumerator.class + end + + test "transform_keys is chainable with Enumerable methods" do + original = { a: 'a', b: 'b' } + mapped = original.transform_keys.with_index { |k, i| [k, i].join.to_sym } + assert_equal({ a0: 'a', b1: 'b' }, mapped) + end +end diff --git a/activesupport/test/core_ext/hash/transform_values_test.rb b/activesupport/test/core_ext/hash/transform_values_test.rb new file mode 100644 index 0000000000..45ed11fef7 --- /dev/null +++ b/activesupport/test/core_ext/hash/transform_values_test.rb @@ -0,0 +1,61 @@ +require 'abstract_unit' +require 'active_support/core_ext/hash/indifferent_access' +require 'active_support/core_ext/hash/transform_values' + +class TransformValuesTest < ActiveSupport::TestCase + test "transform_values returns a new hash with the values computed from the block" do + original = { a: 'a', b: 'b' } + mapped = original.transform_values { |v| v + '!' } + + assert_equal({ a: 'a', b: 'b' }, original) + assert_equal({ a: 'a!', b: 'b!' }, mapped) + end + + test "transform_values! modifies the values of the original" do + original = { a: 'a', b: 'b' } + mapped = original.transform_values! { |v| v + '!' } + + assert_equal({ a: 'a!', b: 'b!' }, original) + assert_same original, mapped + end + + test "indifferent access is still indifferent after mapping values" do + original = { a: 'a', b: 'b' }.with_indifferent_access + mapped = original.transform_values { |v| v + '!' } + + assert_equal 'a!', mapped[:a] + assert_equal 'a!', mapped['a'] + end + + # This is to be consistent with the behavior of Ruby's built in methods + # (e.g. #select, #reject) as of 2.2 + test "default values do not persist during mapping" do + original = Hash.new('foo') + original[:a] = 'a' + mapped = original.transform_values { |v| v + '!' } + + assert_equal 'a!', mapped[:a] + assert_nil mapped[:b] + end + + test "default procs do not persist after mapping" do + original = Hash.new { 'foo' } + original[:a] = 'a' + mapped = original.transform_values { |v| v + '!' } + + assert_equal 'a!', mapped[:a] + assert_nil mapped[:b] + end + + test "transform_values returns an Enumerator if no block is given" do + original = { a: 'a', b: 'b' } + enumerator = original.transform_values + assert_equal Enumerator, enumerator.class + end + + test "transform_values is chainable with Enumerable methods" do + original = { a: 'a', b: 'b' } + mapped = original.transform_values.with_index { |v, i| [v, i].join } + assert_equal({ a: 'a0', b: 'b1' }, mapped) + end +end diff --git a/activesupport/test/core_ext/hash_ext_test.rb b/activesupport/test/core_ext/hash_ext_test.rb index 40c8f03374..3d2f50ce49 100644 --- a/activesupport/test/core_ext/hash_ext_test.rb +++ b/activesupport/test/core_ext/hash_ext_test.rb @@ -23,6 +23,16 @@ class HashExtTest < ActiveSupport::TestCase end end + class HashByConversion + def initialize(hash) + @hash = hash + end + + def to_hash + @hash + end + end + def setup @strings = { 'a' => 1, 'b' => 2 } @nested_strings = { 'a' => { 'b' => { 'c' => 3 } } } @@ -36,6 +46,10 @@ class HashExtTest < ActiveSupport::TestCase @nested_illegal_symbols = { [] => { [] => 3} } @upcase_strings = { 'A' => 1, 'B' => 2 } @nested_upcase_strings = { 'A' => { 'B' => { 'C' => 3 } } } + @string_array_of_hashes = { 'a' => [ { 'b' => 2 }, { 'c' => 3 }, 4 ] } + @symbol_array_of_hashes = { :a => [ { :b => 2 }, { :c => 3 }, 4 ] } + @mixed_array_of_hashes = { :a => [ { :b => 2 }, { 'c' => 3 }, 4 ] } + @upcase_array_of_hashes = { 'A' => [ { 'B' => 2 }, { 'C' => 3 }, 4 ] } end def test_methods @@ -56,6 +70,8 @@ class HashExtTest < ActiveSupport::TestCase assert_respond_to h, :to_options! assert_respond_to h, :compact assert_respond_to h, :compact! + assert_respond_to h, :except + assert_respond_to h, :except! end def test_transform_keys @@ -74,6 +90,9 @@ class HashExtTest < ActiveSupport::TestCase assert_equal @nested_upcase_strings, @nested_symbols.deep_transform_keys{ |key| key.to_s.upcase } assert_equal @nested_upcase_strings, @nested_strings.deep_transform_keys{ |key| key.to_s.upcase } assert_equal @nested_upcase_strings, @nested_mixed.deep_transform_keys{ |key| key.to_s.upcase } + assert_equal @upcase_array_of_hashes, @string_array_of_hashes.deep_transform_keys{ |key| key.to_s.upcase } + assert_equal @upcase_array_of_hashes, @symbol_array_of_hashes.deep_transform_keys{ |key| key.to_s.upcase } + assert_equal @upcase_array_of_hashes, @mixed_array_of_hashes.deep_transform_keys{ |key| key.to_s.upcase } end def test_deep_transform_keys_not_mutates @@ -99,6 +118,9 @@ class HashExtTest < ActiveSupport::TestCase assert_equal @nested_upcase_strings, @nested_symbols.deep_dup.deep_transform_keys!{ |key| key.to_s.upcase } assert_equal @nested_upcase_strings, @nested_strings.deep_dup.deep_transform_keys!{ |key| key.to_s.upcase } assert_equal @nested_upcase_strings, @nested_mixed.deep_dup.deep_transform_keys!{ |key| key.to_s.upcase } + assert_equal @upcase_array_of_hashes, @string_array_of_hashes.deep_dup.deep_transform_keys!{ |key| key.to_s.upcase } + assert_equal @upcase_array_of_hashes, @symbol_array_of_hashes.deep_dup.deep_transform_keys!{ |key| key.to_s.upcase } + assert_equal @upcase_array_of_hashes, @mixed_array_of_hashes.deep_dup.deep_transform_keys!{ |key| key.to_s.upcase } end def test_deep_transform_keys_with_bang_mutates @@ -124,6 +146,9 @@ class HashExtTest < ActiveSupport::TestCase assert_equal @nested_symbols, @nested_symbols.deep_symbolize_keys assert_equal @nested_symbols, @nested_strings.deep_symbolize_keys assert_equal @nested_symbols, @nested_mixed.deep_symbolize_keys + assert_equal @symbol_array_of_hashes, @string_array_of_hashes.deep_symbolize_keys + assert_equal @symbol_array_of_hashes, @symbol_array_of_hashes.deep_symbolize_keys + assert_equal @symbol_array_of_hashes, @mixed_array_of_hashes.deep_symbolize_keys end def test_deep_symbolize_keys_not_mutates @@ -149,6 +174,9 @@ class HashExtTest < ActiveSupport::TestCase assert_equal @nested_symbols, @nested_symbols.deep_dup.deep_symbolize_keys! assert_equal @nested_symbols, @nested_strings.deep_dup.deep_symbolize_keys! assert_equal @nested_symbols, @nested_mixed.deep_dup.deep_symbolize_keys! + assert_equal @symbol_array_of_hashes, @string_array_of_hashes.deep_dup.deep_symbolize_keys! + assert_equal @symbol_array_of_hashes, @symbol_array_of_hashes.deep_dup.deep_symbolize_keys! + assert_equal @symbol_array_of_hashes, @mixed_array_of_hashes.deep_dup.deep_symbolize_keys! end def test_deep_symbolize_keys_with_bang_mutates @@ -194,6 +222,9 @@ class HashExtTest < ActiveSupport::TestCase assert_equal @nested_strings, @nested_symbols.deep_stringify_keys assert_equal @nested_strings, @nested_strings.deep_stringify_keys assert_equal @nested_strings, @nested_mixed.deep_stringify_keys + assert_equal @string_array_of_hashes, @string_array_of_hashes.deep_stringify_keys + assert_equal @string_array_of_hashes, @symbol_array_of_hashes.deep_stringify_keys + assert_equal @string_array_of_hashes, @mixed_array_of_hashes.deep_stringify_keys end def test_deep_stringify_keys_not_mutates @@ -219,6 +250,9 @@ class HashExtTest < ActiveSupport::TestCase assert_equal @nested_strings, @nested_symbols.deep_dup.deep_stringify_keys! assert_equal @nested_strings, @nested_strings.deep_dup.deep_stringify_keys! assert_equal @nested_strings, @nested_mixed.deep_dup.deep_stringify_keys! + assert_equal @string_array_of_hashes, @string_array_of_hashes.deep_dup.deep_stringify_keys! + assert_equal @string_array_of_hashes, @symbol_array_of_hashes.deep_dup.deep_stringify_keys! + assert_equal @string_array_of_hashes, @mixed_array_of_hashes.deep_dup.deep_stringify_keys! end def test_deep_stringify_keys_with_bang_mutates @@ -331,7 +365,7 @@ class HashExtTest < ActiveSupport::TestCase :member? => true } hashes.each do |name, hash| - method_map.sort_by { |m| m.to_s }.each do |meth, expected| + method_map.sort_by(&:to_s).each do |meth, expected| assert_equal(expected, hash.__send__(meth, 'a'), "Calling #{name}.#{meth} 'a'") assert_equal(expected, hash.__send__(meth, :a), @@ -411,6 +445,12 @@ class HashExtTest < ActiveSupport::TestCase assert [updated_with_strings, updated_with_symbols, updated_with_mixed].all? { |h| h.keys.size == 2 } end + def test_update_with_to_hash_conversion + hash = HashWithIndifferentAccess.new + hash.update HashByConversion.new({ :a => 1 }) + assert_equal hash['a'], 1 + end + def test_indifferent_merging hash = HashWithIndifferentAccess.new hash[:a] = 'failure' @@ -430,6 +470,12 @@ class HashExtTest < ActiveSupport::TestCase assert_equal 2, hash['b'] end + def test_merge_with_to_hash_conversion + hash = HashWithIndifferentAccess.new + merged = hash.merge HashByConversion.new({ :a => 1 }) + assert_equal merged['a'], 1 + end + def test_indifferent_replace hash = HashWithIndifferentAccess.new hash[:a] = 42 @@ -442,6 +488,18 @@ class HashExtTest < ActiveSupport::TestCase assert_same hash, replaced end + def test_replace_with_to_hash_conversion + hash = HashWithIndifferentAccess.new + hash[:a] = 42 + + replaced = hash.replace(HashByConversion.new(b: 12)) + + assert hash.key?('b') + assert !hash.key?(:a) + assert_equal 12, hash[:b] + assert_same hash, replaced + end + def test_indifferent_merging_with_block hash = HashWithIndifferentAccess.new hash[:a] = 1 @@ -528,6 +586,8 @@ class HashExtTest < ActiveSupport::TestCase roundtrip = mixed_with_default.with_indifferent_access.to_hash assert_equal @strings, roundtrip assert_equal '1234', roundtrip.default + + # Ensure nested hashes are not HashWithIndiffereneAccess 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) @@ -601,7 +661,15 @@ class HashExtTest < ActiveSupport::TestCase assert_equal 1, h['first'] end - def test_indifferent_subhashes + def test_to_options_on_indifferent_preserves_works_as_hash_with_dup + h = HashWithIndifferentAccess.new({ a: { b: 'b' } }) + dup = h.dup + + dup[:a][:c] = 'c' + assert_equal 'c', h[:a][:c] + end + + def test_indifferent_sub_hashes h = {'user' => {'id' => 5}}.with_indifferent_access ['user', :user].each {|user| [:id, 'id'].each {|id| assert_equal 5, h[user][id], "h[#{user.inspect}][#{id.inspect}] should be 5"}} @@ -625,6 +693,11 @@ class HashExtTest < ActiveSupport::TestCase { :failure => "stuff", :funny => "business" }.assert_valid_keys([ :failure, :funny ]) { :failure => "stuff", :funny => "business" }.assert_valid_keys(:failure, :funny) end + # not all valid keys are required to be present + assert_nothing_raised do + { :failure => "stuff", :funny => "business" }.assert_valid_keys([ :failure, :funny, :sunny ]) + { :failure => "stuff", :funny => "business" }.assert_valid_keys(:failure, :funny, :sunny) + end exception = assert_raise ArgumentError do { :failore => "stuff", :funny => "business" }.assert_valid_keys([ :failure, :funny ]) @@ -635,6 +708,16 @@ class HashExtTest < ActiveSupport::TestCase { :failore => "stuff", :funny => "business" }.assert_valid_keys(:failure, :funny) end assert_equal "Unknown key: :failore. Valid keys are: :failure, :funny", exception.message + + exception = assert_raise ArgumentError do + { :failore => "stuff", :funny => "business" }.assert_valid_keys([ :failure ]) + end + assert_equal "Unknown key: :failore. Valid keys are: :failure", exception.message + + exception = assert_raise ArgumentError do + { :failore => "stuff", :funny => "business" }.assert_valid_keys(:failure) + end + assert_equal "Unknown key: :failore. Valid keys are: :failure", exception.message end def test_assorted_keys_not_stringified @@ -663,6 +746,16 @@ class HashExtTest < ActiveSupport::TestCase assert_equal expected, hash_1 end + def test_deep_merge_with_falsey_values + hash_1 = { e: false } + hash_2 = { e: 'e' } + expected = { e: [:e, false, 'e'] } + assert_equal(expected, hash_1.deep_merge(hash_2) { |k, o, n| [k, o, n] }) + + hash_1.deep_merge!(hash_2) { |k, o, n| [k, o, n] } + assert_equal expected, hash_1 + end + def test_deep_merge_on_indifferent_access hash_1 = HashWithIndifferentAccess.new({ :a => "a", :b => "b", :c => { :c1 => "c1", :c2 => "c2", :c3 => { :d1 => "d1" } } }) hash_2 = HashWithIndifferentAccess.new({ :a => 1, :c => { :c1 => 2, :c3 => { :d2 => "d2" } } }) @@ -853,13 +946,19 @@ class HashExtTest < ActiveSupport::TestCase def test_except_with_more_than_one_argument original = { :a => 'x', :b => 'y', :c => 10 } expected = { :a => 'x' } + assert_equal expected, original.except(:b, :c) + + assert_equal expected, original.except!(:b, :c) + assert_equal expected, original end def test_except_with_original_frozen original = { :a => 'x', :b => 'y' } original.freeze assert_nothing_raised { original.except(:a) } + + assert_raise(RuntimeError) { original.except!(:a) } end def test_except_with_mocha_expectation_on_original @@ -871,11 +970,11 @@ class HashExtTest < ActiveSupport::TestCase def test_compact hash_contain_nil_value = @symbols.merge(z: nil) hash_with_only_nil_values = { a: nil, b: nil } - + h = hash_contain_nil_value.dup assert_equal(@symbols, h.compact) assert_equal(hash_contain_nil_value, h) - + h = hash_with_only_nil_values.dup assert_equal({}, h.compact) assert_equal(hash_with_only_nil_values, h) @@ -884,15 +983,21 @@ class HashExtTest < ActiveSupport::TestCase def test_compact! hash_contain_nil_value = @symbols.merge(z: nil) hash_with_only_nil_values = { a: nil, b: nil } - + h = hash_contain_nil_value.dup assert_equal(@symbols, h.compact!) assert_equal(@symbols, h) - + h = hash_with_only_nil_values.dup assert_equal({}, h.compact!) assert_equal({}, h) end + + def test_new_with_to_hash_conversion + hash = HashWithIndifferentAccess.new(HashByConversion.new(a: 1)) + assert hash.key?('a') + assert_equal 1, hash[:a] + end end class IWriteMyOwnXML @@ -1397,12 +1502,32 @@ class HashToXmlTest < ActiveSupport::TestCase end end + def test_from_xml_array_one + expected = { 'numbers' => { 'type' => 'Array', 'value' => '1' }} + assert_equal expected, Hash.from_xml('<numbers type="Array"><value>1</value></numbers>') + end + + def test_from_xml_array_many + expected = { 'numbers' => { 'type' => 'Array', 'value' => [ '1', '2' ] }} + assert_equal expected, Hash.from_xml('<numbers type="Array"><value>1</value><value>2</value></numbers>') + 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_proc_for_unknown_key + hash_wia = HashWithIndifferentAccess.new { 1 + 2 } + assert_equal 3, hash_wia[:new_key] + end + + def test_should_use_default_proc_if_no_key_is_supplied + hash_wia = HashWithIndifferentAccess.new { 1 + 2 } + assert_equal 3, hash_wia.default + end + def test_should_use_default_value_for_unknown_key hash_wia = HashWithIndifferentAccess.new(3) assert_equal 3, hash_wia[:new_key] @@ -1430,6 +1555,17 @@ class HashToXmlTest < ActiveSupport::TestCase assert_equal 3, hash_wia.default end + def test_should_copy_the_default_proc_when_converting_to_hash_with_indifferent_access + hash = Hash.new do + 2 + 1 + end + assert_equal 3, hash[:foo] + + hash_wia = hash.with_indifferent_access + assert_equal 3, hash_wia[:foo] + assert_equal 3, hash_wia[:bar] + end + # The XML builder seems to fail miserably when trying to tag something # with the same name as a Kernel method (throw, test, loop, select ...) def test_kernel_method_names_to_xml diff --git a/activesupport/test/core_ext/kernel/concern_test.rb b/activesupport/test/core_ext/kernel/concern_test.rb new file mode 100644 index 0000000000..478a00d2d2 --- /dev/null +++ b/activesupport/test/core_ext/kernel/concern_test.rb @@ -0,0 +1,13 @@ +require 'abstract_unit' +require 'active_support/core_ext/kernel/concern' + +class KernelConcernTest < ActiveSupport::TestCase + def test_may_be_defined_at_toplevel + mod = ::TOPLEVEL_BINDING.eval 'concern(:ToplevelConcern) { }' + assert_equal mod, ::ToplevelConcern + assert_kind_of ActiveSupport::Concern, ::ToplevelConcern + assert_not Object.ancestors.include?(::ToplevelConcern), mod.ancestors.inspect + ensure + Object.send :remove_const, :ToplevelConcern + end +end diff --git a/activesupport/test/core_ext/kernel_test.rb b/activesupport/test/core_ext/kernel_test.rb index 18b251173f..503e6595cb 100644 --- a/activesupport/test/core_ext/kernel_test.rb +++ b/activesupport/test/core_ext/kernel_test.rb @@ -15,7 +15,6 @@ class KernelTest < ActiveSupport::TestCase assert_equal old_verbose, $VERBOSE end - def test_enable_warnings enable_warnings { assert_equal true, $VERBOSE } assert_equal 1234, enable_warnings { 1234 } @@ -29,59 +28,11 @@ class KernelTest < ActiveSupport::TestCase assert_equal old_verbose, $VERBOSE end - - def test_silence_stderr - old_stderr_position = STDERR.tell - silence_stderr { STDERR.puts 'hello world' } - assert_equal old_stderr_position, STDERR.tell - rescue Errno::ESPIPE - # Skip if we can't STDERR.tell - end - - def test_silence_stream - old_stream_position = STDOUT.tell - silence_stream(STDOUT) { STDOUT.puts 'hello world' } - assert_equal old_stream_position, STDOUT.tell - rescue Errno::ESPIPE - # Skip if we can't stream.tell - end - - def test_silence_stream_closes_file_descriptors - stream = StringIO.new - dup_stream = StringIO.new - stream.stubs(:dup).returns(dup_stream) - dup_stream.expects(:close) - silence_stream(stream) { stream.puts 'hello world' } - 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 - def test_class_eval o = Object.new class << o; @x = 1; end assert_equal 1, o.class_eval { @x } end - - def test_capture - assert_equal 'STDERR', capture(:stderr) { $stderr.print 'STDERR' } - assert_equal 'STDOUT', capture(:stdout) { print 'STDOUT' } - assert_equal "STDERR\n", capture(:stderr) { system('echo STDERR 1>&2') } - assert_equal "STDOUT\n", capture(:stdout) { system('echo STDOUT') } - end end class KernelSuppressTest < ActiveSupport::TestCase @@ -114,27 +65,3 @@ class MockStdErr puts(message) end end - -class KernelDebuggerTest < ActiveSupport::TestCase - def test_debugger_not_available_message_to_stderr - old_stderr = $stderr - $stderr = MockStdErr.new - debugger - assert_match(/Debugger requested/, $stderr.output.first) - ensure - $stderr = old_stderr - end - - def test_debugger_not_available_message_to_rails_logger - rails = Class.new do - def self.logger - @logger ||= MockStdErr.new - end - end - Object.const_set(:Rails, rails) - debugger - assert_match(/Debugger requested/, rails.logger.output.first) - ensure - Object.send(:remove_const, :Rails) - end -end diff --git a/activesupport/test/core_ext/load_error_test.rb b/activesupport/test/core_ext/load_error_test.rb index 31863d0aca..b2a75a2bcc 100644 --- a/activesupport/test/core_ext/load_error_test.rb +++ b/activesupport/test/core_ext/load_error_test.rb @@ -1,17 +1,11 @@ require 'abstract_unit' require 'active_support/core_ext/load_error' + class TestMissingSourceFile < ActiveSupport::TestCase - def test_with_require - assert_raise(MissingSourceFile) { require 'no_this_file_don\'t_exist' } - end - def test_with_load - assert_raise(MissingSourceFile) { load 'nor_does_this_one' } - end - def test_path - begin load 'nor/this/one.rb' - rescue MissingSourceFile => e - assert_equal 'nor/this/one.rb', e.path + def test_it_is_deprecated + assert_deprecated do + MissingSourceFile.new end end end @@ -29,4 +23,4 @@ class TestLoadError < ActiveSupport::TestCase assert_equal 'nor/this/one.rb', e.path end end -end
\ No newline at end of file +end diff --git a/activesupport/test/core_ext/module/concerning_test.rb b/activesupport/test/core_ext/module/concerning_test.rb index c6863b24a4..07d860b71c 100644 --- a/activesupport/test/core_ext/module/concerning_test.rb +++ b/activesupport/test/core_ext/module/concerning_test.rb @@ -1,35 +1,65 @@ require 'abstract_unit' require 'active_support/core_ext/module/concerning' -class ConcerningTest < ActiveSupport::TestCase - def test_concern_shortcut_creates_a_module_but_doesnt_include_it - mod = Module.new { concern(:Foo) { } } - assert_kind_of Module, mod::Foo - assert mod::Foo.respond_to?(:included) - assert !mod.ancestors.include?(mod::Foo), mod.ancestors.inspect +class ModuleConcerningTest < ActiveSupport::TestCase + def test_concerning_declares_a_concern_and_includes_it_immediately + klass = Class.new { concerning(:Foo) { } } + assert klass.ancestors.include?(klass::Foo), klass.ancestors.inspect end +end +class ModuleConcernTest < ActiveSupport::TestCase def test_concern_creates_a_module_extended_with_active_support_concern klass = Class.new do - concern :Foo do + concern :Baz do included { @foo = 1 } def should_be_public; end end end # Declares a concern but doesn't include it - assert_kind_of Module, klass::Foo - assert !klass.ancestors.include?(klass::Foo), klass.ancestors.inspect + assert klass.const_defined?(:Baz, false) + assert !ModuleConcernTest.const_defined?(:Baz) + assert_kind_of ActiveSupport::Concern, klass::Baz + assert !klass.ancestors.include?(klass::Baz), klass.ancestors.inspect # Public method visibility by default - assert klass::Foo.public_instance_methods.map(&:to_s).include?('should_be_public') + assert klass::Baz.public_instance_methods.map(&:to_s).include?('should_be_public') # Calls included hook - assert_equal 1, Class.new { include klass::Foo }.instance_variable_get('@foo') + assert_equal 1, Class.new { include klass::Baz }.instance_variable_get('@foo') end - def test_concerning_declares_a_concern_and_includes_it_immediately - klass = Class.new { concerning(:Foo) { } } - assert klass.ancestors.include?(klass::Foo), klass.ancestors.inspect + class Foo + concerning :Bar do + module ClassMethods + def will_be_orphaned; end + end + + const_set :ClassMethods, Module.new { + def hacked_on; end + } + + # Doesn't overwrite existing ClassMethods module. + class_methods do + def nicer_dsl; end + end + + # Doesn't overwrite previous class_methods definitions. + class_methods do + def doesnt_clobber; end + end + end + end + + def test_using_class_methods_blocks_instead_of_ClassMethods_module + assert !Foo.respond_to?(:will_be_orphaned) + assert Foo.respond_to?(:hacked_on) + assert Foo.respond_to?(:nicer_dsl) + assert Foo.respond_to?(:doesnt_clobber) + + # Orphan in Foo::ClassMethods, not Bar::ClassMethods. + assert Foo.const_defined?(:ClassMethods) + assert Foo::ClassMethods.method_defined?(:will_be_orphaned) end end diff --git a/activesupport/test/core_ext/module_test.rb b/activesupport/test/core_ext/module_test.rb index 5b99fae411..3c49c4d14f 100644 --- a/activesupport/test/core_ext/module_test.rb +++ b/activesupport/test/core_ext/module_test.rb @@ -56,8 +56,14 @@ Developer = Struct.new(:client) do delegate :name, :to => :client, :prefix => nil end +Event = Struct.new(:case) do + delegate :foo, :to => :case +end + Tester = Struct.new(:client) do delegate :name, :to => :client, :prefix => false + + def foo; 1; end end Product = Struct.new(:name) do @@ -72,7 +78,7 @@ Product = Struct.new(:name) do def type @type ||= begin - nil.type_name + :thing_without_same_method_name_as_delegated.name end end end @@ -245,6 +251,16 @@ class ModuleTest < ActiveSupport::TestCase end end + def test_delegation_line_number + _, line = Someone.instance_method(:foo).source_location + assert_equal Someone::FAILED_DELEGATE_LINE, line + end + + def test_delegate_line_with_nil + _, line = Someone.instance_method(:bar).source_location + assert_equal Someone::FAILED_DELEGATE_LINE_2, line + end + def test_delegation_exception_backtrace someone = Someone.new("foo", "bar") someone.foo @@ -485,4 +501,9 @@ class MethodAliasingTest < ActiveSupport::TestCase assert_equal 'duck_with_orange', @instance.duck assert FooClassWithBarMethod.public_method_defined?(:duck) end + + def test_delegate_with_case + event = Event.new(Tester.new) + assert_equal 1, event.foo + end end diff --git a/activesupport/test/core_ext/numeric_ext_test.rb b/activesupport/test/core_ext/numeric_ext_test.rb index 3b1dabea8d..b82448458d 100644 --- a/activesupport/test/core_ext/numeric_ext_test.rb +++ b/activesupport/test/core_ext/numeric_ext_test.rb @@ -22,18 +22,6 @@ class NumericExtTimeAndDateTimeTest < ActiveSupport::TestCase end end - def test_deprecated_since_and_ago - assert_equal @now + 1, assert_deprecated { 1.since(@now) } - assert_equal @now - 1, assert_deprecated { 1.ago(@now) } - end - - def test_deprecated_since_and_ago_without_argument - now = Time.now - assert assert_deprecated { 1.since } >= now + 1 - now = Time.now - assert assert_deprecated { 1.ago } >= now - 1 - end - def test_irregular_durations assert_equal @now.advance(:days => 3000), 3000.days.since(@now) assert_equal @now.advance(:months => 1), 1.month.since(@now) @@ -83,44 +71,6 @@ class NumericExtTimeAndDateTimeTest < ActiveSupport::TestCase assert_equal Time.utc(2005,2,28,15,15,10), Time.utc(2004,2,29,15,15,10) + 1.year assert_equal DateTime.civil(2005,2,28,15,15,10), DateTime.civil(2004,2,29,15,15,10) + 1.year end - - def test_since_and_ago_anchored_to_time_now_when_time_zone_is_not_set - Time.zone = nil - with_env_tz 'US/Eastern' do - Time.stubs(:now).returns Time.local(2000) - # since - assert_not_instance_of ActiveSupport::TimeWithZone, assert_deprecated { 5.since } - assert_equal Time.local(2000,1,1,0,0,5), assert_deprecated { 5.since } - # ago - assert_not_instance_of ActiveSupport::TimeWithZone, assert_deprecated { 5.ago } - assert_equal Time.local(1999,12,31,23,59,55), assert_deprecated { 5.ago } - end - end - - def test_since_and_ago_anchored_to_time_zone_now_when_time_zone_is_set - Time.zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)'] - with_env_tz 'US/Eastern' do - Time.stubs(:now).returns Time.local(2000) - # since - assert_instance_of ActiveSupport::TimeWithZone, assert_deprecated { 5.since } - assert_equal Time.utc(2000,1,1,0,0,5), assert_deprecated { 5.since.time } - assert_equal 'Eastern Time (US & Canada)', assert_deprecated { 5.since.time_zone.name } - # ago - assert_instance_of ActiveSupport::TimeWithZone, assert_deprecated { 5.ago } - assert_equal Time.utc(1999,12,31,23,59,55), assert_deprecated { 5.ago.time } - assert_equal 'Eastern Time (US & Canada)', assert_deprecated { 5.ago.time_zone.name } - end - ensure - Time.zone = nil - 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 NumericExtDateTest < ActiveSupport::TestCase @@ -435,7 +385,7 @@ class NumericExtFormattingTest < ActiveSupport::TestCase assert_equal BigDecimal, BigDecimal("1000010").class assert_equal '1 Million', BigDecimal("1000010").to_s(:human) end - + def test_in_milliseconds assert_equal 10_000, 10.seconds.in_milliseconds end diff --git a/activesupport/test/core_ext/object/acts_like_test.rb b/activesupport/test/core_ext/object/acts_like_test.rb new file mode 100644 index 0000000000..e68b1d23cb --- /dev/null +++ b/activesupport/test/core_ext/object/acts_like_test.rb @@ -0,0 +1,33 @@ +require 'abstract_unit' +require 'active_support/core_ext/object' + +class ObjectTests < ActiveSupport::TestCase + class DuckTime + def acts_like_time? + true + end + end + + def test_duck_typing + object = Object.new + time = Time.now + date = Date.today + dt = DateTime.new + duck = DuckTime.new + + assert !object.acts_like?(:time) + assert !object.acts_like?(:date) + + assert time.acts_like?(:time) + assert !time.acts_like?(:date) + + assert !date.acts_like?(:time) + assert date.acts_like?(:date) + + assert dt.acts_like?(:time) + assert dt.acts_like?(:date) + + assert duck.acts_like?(:time) + assert !duck.acts_like?(:date) + end +end diff --git a/activesupport/test/core_ext/blank_test.rb b/activesupport/test/core_ext/object/blank_test.rb index 246bc7fa61..246bc7fa61 100644 --- a/activesupport/test/core_ext/blank_test.rb +++ b/activesupport/test/core_ext/object/blank_test.rb diff --git a/activesupport/test/core_ext/deep_dup_test.rb b/activesupport/test/core_ext/object/deep_dup_test.rb index 91d558dbb5..91d558dbb5 100644 --- a/activesupport/test/core_ext/deep_dup_test.rb +++ b/activesupport/test/core_ext/object/deep_dup_test.rb diff --git a/activesupport/test/core_ext/object/duplicable_test.rb b/activesupport/test/core_ext/object/duplicable_test.rb new file mode 100644 index 0000000000..d37f4bd0d8 --- /dev/null +++ b/activesupport/test/core_ext/object/duplicable_test.rb @@ -0,0 +1,22 @@ +require 'abstract_unit' +require 'bigdecimal' +require 'active_support/core_ext/object/duplicable' +require 'active_support/core_ext/numeric/time' + +class DuplicableTest < ActiveSupport::TestCase + RAISE_DUP = [nil, false, true, :symbol, 1, 2.3, method(:puts)] + ALLOW_DUP = ['1', Object.new, /foo/, [], {}, Time.now, Class.new, Module.new] + ALLOW_DUP << BigDecimal.new('4.56') + + def test_duplicable + RAISE_DUP.each do |v| + assert !v.duplicable? + assert_raises(TypeError, v.class.name) { v.dup } + end + + ALLOW_DUP.each do |v| + assert v.duplicable?, "#{ v.class } should be duplicable" + assert_nothing_raised { v.dup } + end + end +end diff --git a/activesupport/test/core_ext/object/inclusion_test.rb b/activesupport/test/core_ext/object/inclusion_test.rb index 478706eeae..32d512eca3 100644 --- a/activesupport/test/core_ext/object/inclusion_test.rb +++ b/activesupport/test/core_ext/object/inclusion_test.rb @@ -37,14 +37,23 @@ class InTest < ActiveSupport::TestCase end class C < B end + class D + end def test_in_module assert A.in?(B) assert A.in?(C) assert !A.in?(A) + assert !A.in?(D) end def test_no_method_catching assert_raise(ArgumentError) { 1.in?(1) } end + + def test_presence_in + assert_equal "stuff", "stuff".presence_in(%w( lots of stuff )) + assert_nil "stuff".presence_in(%w( lots of crap )) + assert_raise(ArgumentError) { 1.presence_in(1) } + end end diff --git a/activesupport/test/core_ext/object/instance_variables_test.rb b/activesupport/test/core_ext/object/instance_variables_test.rb new file mode 100644 index 0000000000..9f4c5dc4f1 --- /dev/null +++ b/activesupport/test/core_ext/object/instance_variables_test.rb @@ -0,0 +1,31 @@ +require 'abstract_unit' +require 'active_support/core_ext/object' + +class ObjectInstanceVariableTest < ActiveSupport::TestCase + def setup + @source, @dest = Object.new, Object.new + @source.instance_variable_set(:@bar, 'bar') + @source.instance_variable_set(:@baz, 'baz') + end + + def test_instance_variable_names + assert_equal %w(@bar @baz), @source.instance_variable_names.sort + end + + def test_instance_values + assert_equal({'bar' => 'bar', 'baz' => 'baz'}, @source.instance_values) + end + + def test_instance_exec_passes_arguments_to_block + assert_equal %w(hello goodbye), 'hello'.instance_exec('goodbye') { |v| [self, v] } + end + + def test_instance_exec_with_frozen_obj + assert_equal %w(olleh goodbye), 'hello'.freeze.instance_exec('goodbye') { |v| [reverse, v] } + end + + def test_instance_exec_nested + assert_equal %w(goodbye olleh bar), 'hello'.instance_exec('goodbye') { |arg| + [arg] + instance_exec('bar') { |v| [reverse, v] } } + end +end diff --git a/activesupport/test/core_ext/object/json_cherry_pick_test.rb b/activesupport/test/core_ext/object/json_cherry_pick_test.rb new file mode 100644 index 0000000000..2f7ea3a497 --- /dev/null +++ b/activesupport/test/core_ext/object/json_cherry_pick_test.rb @@ -0,0 +1,42 @@ +require 'abstract_unit' + +# These test cases were added to test that cherry-picking the json extensions +# works correctly, primarily for dependencies problems reported in #16131. They +# need to be executed in isolation to reproduce the scenario correctly, because +# other test cases might have already loaded additional dependencies. + +class JsonCherryPickTest < ActiveSupport::TestCase + include ActiveSupport::Testing::Isolation + + def test_time_as_json + require_or_skip 'active_support/core_ext/object/json' + + expected = Time.new(2004, 7, 25) + actual = Time.parse(expected.as_json) + + assert_equal expected, actual + end + + def test_date_as_json + require_or_skip 'active_support/core_ext/object/json' + + expected = Date.new(2004, 7, 25) + actual = Date.parse(expected.as_json) + + assert_equal expected, actual + end + + def test_datetime_as_json + require_or_skip 'active_support/core_ext/object/json' + + expected = DateTime.new(2004, 7, 25) + actual = DateTime.parse(expected.as_json) + + assert_equal expected, actual + end + + private + def require_or_skip(file) + require(file) || skip("'#{file}' was already loaded") + end +end diff --git a/activesupport/test/core_ext/object/json_test.rb b/activesupport/test/core_ext/object/json_test.rb deleted file mode 100644 index d3d31530df..0000000000 --- a/activesupport/test/core_ext/object/json_test.rb +++ /dev/null @@ -1,9 +0,0 @@ -require 'abstract_unit' - -class JsonTest < ActiveSupport::TestCase - # See activesupport/test/json/encoding_test.rb for JSON encoding tests - - def test_deprecated_require_to_json_rb - assert_deprecated { require 'active_support/core_ext/object/to_json' } - end -end diff --git a/activesupport/test/core_ext/object/to_param_test.rb b/activesupport/test/core_ext/object/to_param_test.rb index bd7c6c422a..30a7557dc2 100644 --- a/activesupport/test/core_ext/object/to_param_test.rb +++ b/activesupport/test/core_ext/object/to_param_test.rb @@ -2,6 +2,12 @@ require 'abstract_unit' require 'active_support/core_ext/object/to_param' class ToParamTest < ActiveSupport::TestCase + class CustomString < String + def to_param + "custom-#{ self }" + end + end + def test_object foo = Object.new def foo.to_s; 'foo' end @@ -16,4 +22,16 @@ class ToParamTest < ActiveSupport::TestCase assert_equal true, true.to_param assert_equal false, false.to_param end + + def test_array + # Empty Array + assert_equal '', [].to_param + + array = [1, 2, 3, 4] + assert_equal "1/2/3/4", array.to_param + + # Array of different objects + array = [1, '3', { a: 1, b: 2 }, nil, true, false, CustomString.new('object')] + assert_equal "1/3/a=1&b=2//true/false/custom-object", array.to_param + end end diff --git a/activesupport/test/core_ext/object/to_query_test.rb b/activesupport/test/core_ext/object/to_query_test.rb index 92f996f9a4..09cab3ed35 100644 --- a/activesupport/test/core_ext/object/to_query_test.rb +++ b/activesupport/test/core_ext/object/to_query_test.rb @@ -46,6 +46,35 @@ class ToQueryTest < ActiveSupport::TestCase :person => {:id => [20, 10]} end + def test_empty_array + assert_equal "person%5B%5D=", [].to_query('person') + end + + def test_nested_empty_hash + assert_equal '', + {}.to_query + assert_query_equal 'a=1&b%5Bc%5D=3', + { a: 1, b: { c: 3, d: {} } } + assert_query_equal '', + { a: {b: {c: {}}} } + assert_query_equal 'b%5Bc%5D=false&b%5Be%5D=&b%5Bf%5D=&p=12', + { p: 12, b: { c: false, e: nil, f: '' } } + assert_query_equal 'b%5Bc%5D=3&b%5Bf%5D=', + { b: { c: 3, k: {}, f: '' } } + assert_query_equal 'b=3', + {a: [], b: 3} + end + + def test_hash_with_namespace + hash = { name: 'Nakshay', nationality: 'Indian' } + assert_equal "user%5Bname%5D=Nakshay&user%5Bnationality%5D=Indian", hash.to_query('user') + end + + def test_hash_sorted_lexicographically + hash = { type: 'human', name: 'Nakshay' } + assert_equal "name=Nakshay&type=human", hash.to_query + end + private def assert_query_equal(expected, actual) assert_equal expected.split('&'), actual.to_query.split('&') diff --git a/activesupport/test/core_ext/object_and_class_ext_test.rb b/activesupport/test/core_ext/object/try_test.rb index 0f454fdd95..89438675c1 100644 --- a/activesupport/test/core_ext/object_and_class_ext_test.rb +++ b/activesupport/test/core_ext/object/try_test.rb @@ -1,70 +1,5 @@ require 'abstract_unit' -require 'active_support/time' require 'active_support/core_ext/object' -require 'active_support/core_ext/class/subclasses' - -class ObjectTests < ActiveSupport::TestCase - class DuckTime - def acts_like_time? - true - end - end - - def test_duck_typing - object = Object.new - time = Time.now - date = Date.today - dt = DateTime.new - duck = DuckTime.new - - assert !object.acts_like?(:time) - assert !object.acts_like?(:date) - - assert time.acts_like?(:time) - assert !time.acts_like?(:date) - - assert !date.acts_like?(:time) - assert date.acts_like?(:date) - - assert dt.acts_like?(:time) - assert dt.acts_like?(:date) - - assert duck.acts_like?(:time) - assert !duck.acts_like?(:date) - end -end - -class ObjectInstanceVariableTest < ActiveSupport::TestCase - def setup - @source, @dest = Object.new, Object.new - @source.instance_variable_set(:@bar, 'bar') - @source.instance_variable_set(:@baz, 'baz') - end - - def test_instance_variable_names - assert_equal %w(@bar @baz), @source.instance_variable_names.sort - end - - def test_instance_values - object = Object.new - object.instance_variable_set :@a, 1 - object.instance_variable_set :@b, 2 - assert_equal({'a' => 1, 'b' => 2}, object.instance_values) - end - - def test_instance_exec_passes_arguments_to_block - assert_equal %w(hello goodbye), 'hello'.instance_exec('goodbye') { |v| [self, v] } - end - - def test_instance_exec_with_frozen_obj - assert_equal %w(olleh goodbye), 'hello'.freeze.instance_exec('goodbye') { |v| [reverse, v] } - end - - def test_instance_exec_nested - assert_equal %w(goodbye olleh bar), 'hello'.instance_exec('goodbye') { |arg| - [arg] + instance_exec('bar') { |v| [reverse, v] } } - end -end class ObjectTryTest < ActiveSupport::TestCase def setup @@ -95,10 +30,6 @@ 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 @@ -121,7 +52,11 @@ class ObjectTryTest < ActiveSupport::TestCase end def test_try_only_block - assert_equal @string.reverse, @string.try { |s| s.reverse } + assert_equal @string.reverse, @string.try(&:reverse) + end + + def test_try_only_block_bang + assert_equal @string.reverse, @string.try!(&:reverse) end def test_try_only_block_nil @@ -130,6 +65,14 @@ class ObjectTryTest < ActiveSupport::TestCase assert_equal false, ran end + def test_try_with_instance_eval_block + assert_equal @string.reverse, @string.try { reverse } + end + + def test_try_with_instance_eval_block_bang + assert_equal @string.reverse, @string.try! { reverse } + end + def test_try_with_private_method_bang klass = Class.new do private @@ -141,7 +84,7 @@ class ObjectTryTest < ActiveSupport::TestCase assert_raise(NoMethodError) { klass.new.try!(:private_method) } end - + def test_try_with_private_method klass = Class.new do private diff --git a/activesupport/test/core_ext/range_ext_test.rb b/activesupport/test/core_ext/range_ext_test.rb index 150e6b65fb..f096328cee 100644 --- a/activesupport/test/core_ext/range_ext_test.rb +++ b/activesupport/test/core_ext/range_ext_test.rb @@ -12,10 +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 + assert_instance_of Range, DateTime.new..DateTime::Infinity.new end def test_overlaps_last_inclusive @@ -114,6 +115,11 @@ class RangeTest < ActiveSupport::TestCase def test_date_time_with_each datetime = DateTime.now - assert ((datetime - 1.hour)..datetime).each {} + assert(((datetime - 1.hour)..datetime).each {}) + end + + def test_date_time_with_step + datetime = DateTime.now + assert(((datetime - 1.hour)..datetime).step(1) {}) end end diff --git a/activesupport/test/core_ext/securerandom.rb b/activesupport/test/core_ext/securerandom.rb new file mode 100644 index 0000000000..dfacb7fe9f --- /dev/null +++ b/activesupport/test/core_ext/securerandom.rb @@ -0,0 +1,20 @@ +require 'abstract_unit' +require 'active_support/core_ext/securerandom' + +class SecureRandomTest < ActiveSupport::TestCase + def test_base58 + s1 = SecureRandom.base58 + s2 = SecureRandom.base58 + + assert_not_equal s1, s2 + assert_equal 16, s1.length + end + + def test_base58_with_length + s1 = SecureRandom.base58(24) + s2 = SecureRandom.base58(24) + + assert_not_equal s1, s2 + assert_equal 24, s1.length + end +end diff --git a/activesupport/test/core_ext/string_ext_test.rb b/activesupport/test/core_ext/string_ext_test.rb index d4f8ba8cdd..cdc695f036 100644 --- a/activesupport/test/core_ext/string_ext_test.rb +++ b/activesupport/test/core_ext/string_ext_test.rb @@ -10,10 +10,12 @@ require 'active_support/time' require 'active_support/core_ext/string/strip' require 'active_support/core_ext/string/output_safety' require 'active_support/core_ext/string/indent' +require 'time_zone_test_helpers' class StringInflectionsTest < ActiveSupport::TestCase include InflectorTestCases include ConstantizeTestCases + include TimeZoneTestHelpers def test_strip_heredoc_on_an_empty_string assert_equal '', ''.strip_heredoc @@ -58,6 +60,11 @@ class StringInflectionsTest < ActiveSupport::TestCase assert_equal("blargles", "blargle".pluralize(2)) end + test 'pluralize with count = 1 still returns new string' do + name = "Kuldeep" + assert_not_same name.pluralize(1), name + end + def test_singularize SingularToPlural.each do |singular, plural| assert_equal(singular, plural.singularize) @@ -161,6 +168,10 @@ class StringInflectionsTest < ActiveSupport::TestCase end end + def test_humanize_with_html_escape + assert_equal 'Hello', ERB::Util.html_escape("hello").humanize + end + def test_ord assert_equal 97, 'a'.ord assert_equal 97, 'abc'.ord @@ -178,21 +189,21 @@ class StringInflectionsTest < ActiveSupport::TestCase end def test_string_squish - 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} + original = %{\u205f\u3000 A string surrounded by various unicode spaces, + with tabs(\t\t), newlines(\n\n), unicode nextlines(\u0085\u0085) and many spaces( ). \u00a0\u2007} - expected = "A string surrounded by unicode mongolian vowel separators, " + + expected = "A string surrounded by various unicode spaces, " + "with tabs( ), newlines( ), unicode nextlines( ) and many spaces( )." # Make sure squish returns what we expect: - assert_equal original.squish, expected + assert_equal expected, original.squish # But doesn't modify the original string: - assert_not_equal original, expected + assert_not_equal expected, original # Make sure squish! returns what we expect: - assert_equal original.squish!, expected + assert_equal expected, original.squish! # And changes the original string: - assert_equal original, expected + assert_equal expected, original end def test_string_inquiry @@ -205,19 +216,40 @@ class StringInflectionsTest < ActiveSupport::TestCase assert_equal "Hello Wor...", "Hello World!!".truncate(12) end - def test_truncate_with_omission_and_seperator + def test_truncate_with_omission_and_separator assert_equal "Hello[...]", "Hello World!".truncate(10, :omission => "[...]") assert_equal "Hello[...]", "Hello Big World!".truncate(13, :omission => "[...]", :separator => ' ') assert_equal "Hello Big[...]", "Hello Big World!".truncate(14, :omission => "[...]", :separator => ' ') assert_equal "Hello Big[...]", "Hello Big World!".truncate(15, :omission => "[...]", :separator => ' ') end - def test_truncate_with_omission_and_regexp_seperator + def test_truncate_with_omission_and_regexp_separator assert_equal "Hello[...]", "Hello Big World!".truncate(13, :omission => "[...]", :separator => /\s/) assert_equal "Hello Big[...]", "Hello Big World!".truncate(14, :omission => "[...]", :separator => /\s/) assert_equal "Hello Big[...]", "Hello Big World!".truncate(15, :omission => "[...]", :separator => /\s/) end + def test_truncate_words + assert_equal "Hello Big World!", "Hello Big World!".truncate_words(3) + assert_equal "Hello Big...", "Hello Big World!".truncate_words(2) + end + + def test_truncate_words_with_omission + assert_equal "Hello Big World!", "Hello Big World!".truncate_words(3, :omission => "[...]") + assert_equal "Hello Big[...]", "Hello Big World!".truncate_words(2, :omission => "[...]") + end + + def test_truncate_words_with_separator + assert_equal "Hello<br>Big<br>World!...", "Hello<br>Big<br>World!<br>".truncate_words(3, :separator => '<br>') + assert_equal "Hello<br>Big<br>World!", "Hello<br>Big<br>World!".truncate_words(3, :separator => '<br>') + assert_equal "Hello\n<br>Big...", "Hello\n<br>Big<br>Wide<br>World!".truncate_words(2, :separator => '<br>') + end + + def test_truncate_words_with_separator_and_omission + assert_equal "Hello<br>Big<br>World![...]", "Hello<br>Big<br>World!<br>".truncate_words(3, :omission => "[...]", :separator => '<br>') + assert_equal "Hello<br>Big<br>World!", "Hello<br>Big<br>World!".truncate_words(3, :omission => "[...]", :separator => '<br>') + end + def test_truncate_multibyte assert_equal "\354\225\204\353\246\254\353\236\221 \354\225\204\353\246\254 ...".force_encoding(Encoding::UTF_8), "\354\225\204\353\246\254\353\236\221 \354\225\204\353\246\254 \354\225\204\353\235\274\353\246\254\354\230\244".force_encoding(Encoding::UTF_8).truncate(10) @@ -228,20 +260,32 @@ class StringInflectionsTest < ActiveSupport::TestCase end def test_remove - assert_equal "Summer", "Fast Summer".remove(/Fast /) - assert_equal "Summer", "Fast Summer".remove!(/Fast /) + original = "This is a good day to die" + assert_equal "This is a good day", original.remove(" to die") + assert_equal "This is a good day", original.remove(" to ", /die/) + assert_equal "This is a good day to die", original + end + + def test_remove_for_multiple_occurrences + original = "This is a good day to die to die" + assert_equal "This is a good day", original.remove(" to die") + assert_equal "This is a good day to die to die", original + end + + def test_remove! + original = "This is a very good day to die" + assert_equal "This is a good day to die", original.remove!(" very") + assert_equal "This is a good day to die", original + assert_equal "This is a good day", original.remove!(" to ", /die/) + assert_equal "This is a good day", original end def test_constantize - run_constantize_tests_on do |string| - string.constantize - end + run_constantize_tests_on(&:constantize) end def test_safe_constantize - run_safe_constantize_tests_on do |string| - string.safe_constantize - end + run_safe_constantize_tests_on(&:safe_constantize) end end @@ -292,6 +336,12 @@ class StringAccessTest < ActiveSupport::TestCase assert_equal 'x', 'x'.first(4) end + test "#first with Fixnum >= string length still returns a new string" do + string = "hello" + different_string = string.first(5) + assert_not_same different_string, string + end + test "#last returns the last character" do assert_equal "o", "hello".last assert_equal 'x', 'x'.last @@ -304,6 +354,12 @@ class StringAccessTest < ActiveSupport::TestCase assert_equal 'x', 'x'.last(4) end + test "#last with Fixnum >= string length still returns a new string" do + string = "hello" + different_string = string.last(5) + assert_not_same different_string, string + end + test "access returns a real string" do hash = {} hash["h"] = true @@ -333,6 +389,8 @@ class StringAccessTest < ActiveSupport::TestCase end class StringConversionsTest < ActiveSupport::TestCase + include TimeZoneTestHelpers + 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) @@ -358,11 +416,11 @@ class StringConversionsTest < ActiveSupport::TestCase end def test_partial_string_to_time - with_env_tz "Europe/Moscow" do + with_env_tz "Europe/Moscow" do # use timezone which does not observe DST. 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.local(now.year, now.month, now.day, 17, 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 @@ -502,14 +560,6 @@ class StringConversionsTest < ActiveSupport::TestCase 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 @@ -604,6 +654,19 @@ class OutputSafetyTest < ActiveSupport::TestCase assert !@other_combination.html_safe? end + test "Prepending safe onto unsafe yields unsafe" do + @string.prepend "other".html_safe + assert !@string.html_safe? + assert_equal @string, "otherhello" + end + + test "Prepending unsafe onto safe yields escaped safe" do + other = "other".html_safe + other.prepend "<foo>" + assert other.html_safe? + assert_equal other, "<foo>other" + end + test "Concatting safe onto unsafe yields unsafe" do @other_string = "other" @@ -714,6 +777,14 @@ class OutputSafetyTest < ActiveSupport::TestCase string = "<b>hello</b>".html_safe assert_equal string, ERB::Util.html_escape(string) end + + test "ERB::Util.html_escape_once only escapes once" do + string = '1 < 2 & 3' + escaped_string = "1 < 2 & 3" + + assert_equal escaped_string, ERB::Util.html_escape_once(string) + assert_equal escaped_string, ERB::Util.html_escape_once(escaped_string) + end end class StringExcludeTest < ActiveSupport::TestCase diff --git a/activesupport/test/core_ext/struct_test.rb b/activesupport/test/core_ext/struct_test.rb deleted file mode 100644 index 0dff7b32d2..0000000000 --- a/activesupport/test/core_ext/struct_test.rb +++ /dev/null @@ -1,10 +0,0 @@ -require 'abstract_unit' -require 'active_support/core_ext/struct' - -class StructExt < ActiveSupport::TestCase - def test_to_h - x = Struct.new(:foo, :bar) - z = x.new(1, 2) - assert_equal({ foo: 1, bar: 2 }, z.to_h) - end -end diff --git a/activesupport/test/core_ext/thread_test.rb b/activesupport/test/core_ext/thread_test.rb deleted file mode 100644 index 6a7c6e0604..0000000000 --- a/activesupport/test/core_ext/thread_test.rb +++ /dev/null @@ -1,75 +0,0 @@ -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 e0a4b1be3e..d59775001b 100644 --- a/activesupport/test/core_ext/time_ext_test.rb +++ b/activesupport/test/core_ext/time_ext_test.rb @@ -1,6 +1,7 @@ require 'abstract_unit' require 'active_support/time' require 'core_ext/date_and_time_behavior' +require 'time_zone_test_helpers' class TimeExtCalculationsTest < ActiveSupport::TestCase def date_time_init(year,month,day,hour,minute,second,usec=0) @@ -8,6 +9,7 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase end include DateAndTimeBehavior + include TimeZoneTestHelpers def test_seconds_since_midnight assert_equal 1,Time.local(2005,1,1,0,0,1).seconds_since_midnight @@ -385,6 +387,8 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase assert_equal Time.local(2005,1,2,11, 6, 0, 0), Time.local(2005,1,2,11,22,33,44).change(:min => 6) assert_equal Time.local(2005,1,2,11,22, 7, 0), Time.local(2005,1,2,11,22,33,44).change(:sec => 7) assert_equal Time.local(2005,1,2,11,22,33, 8), Time.local(2005,1,2,11,22,33,44).change(:usec => 8) + assert_equal Time.local(2005,1,2,11,22,33, 8), Time.local(2005,1,2,11,22,33,2).change(:nsec => 8000) + assert_raise(ArgumentError) { Time.local(2005,1,2,11,22,33, 8).change(:usec => 1, :nsec => 1) } end def test_utc_change @@ -394,6 +398,7 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase assert_equal Time.utc(2005,2,22,16), Time.utc(2005,2,22,15,15,10).change(:hour => 16) assert_equal Time.utc(2005,2,22,16,45), Time.utc(2005,2,22,15,15,10).change(:hour => 16, :min => 45) assert_equal Time.utc(2005,2,22,15,45), Time.utc(2005,2,22,15,15,10).change(:min => 45) + assert_equal Time.utc(2005,1,2,11,22,33,8), Time.utc(2005,1,2,11,22,33,2).change(:nsec => 8000) end def test_offset_change @@ -403,6 +408,11 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase assert_equal Time.new(2005,2,22,16,0,0,"-08:00"), Time.new(2005,2,22,15,15,10,"-08:00").change(:hour => 16) assert_equal Time.new(2005,2,22,16,45,0,"-08:00"), Time.new(2005,2,22,15,15,10,"-08:00").change(:hour => 16, :min => 45) assert_equal Time.new(2005,2,22,15,45,0,"-08:00"), Time.new(2005,2,22,15,15,10,"-08:00").change(:min => 45) + assert_equal Time.new(2005,2,22,15,15,10,"-08:00"), Time.new(2005,2,22,15,15,0,"-08:00").change(:sec => 10) + assert_equal 10, Time.new(2005,2,22,15,15,0,"-08:00").change(:usec => 10).usec + assert_equal 10, Time.new(2005,2,22,15,15,0,"-08:00").change(:nsec => 10).nsec + assert_raise(ArgumentError) { Time.new(2005, 2, 22, 15, 15, 45, "-08:00").change(:usec => 1000000) } + assert_raise(ArgumentError) { Time.new(2005, 2, 22, 15, 15, 45, "-08:00").change(:nsec => 1000000000) } end def test_advance @@ -847,15 +857,6 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase def test_all_year assert_equal Time.local(2011,1,1,0,0,0)..Time.local(2011,12,31,23,59,59,Rational(999999999, 1000)), Time.local(2011,6,7,10,10,10).all_year 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 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 5494824a40..92c233d567 100644 --- a/activesupport/test/core_ext/time_with_zone_test.rb +++ b/activesupport/test/core_ext/time_with_zone_test.rb @@ -1,8 +1,9 @@ require 'abstract_unit' require 'active_support/time' -require 'active_support/json' +require 'time_zone_test_helpers' class TimeWithZoneTest < ActiveSupport::TestCase + include TimeZoneTestHelpers def setup @utc = Time.utc(2000, 1, 1, 0) @@ -66,25 +67,6 @@ class TimeWithZoneTest < ActiveSupport::TestCase assert_equal 'EDT', ActiveSupport::TimeWithZone.new(Time.utc(2000, 6), @time_zone).zone #dst end - def test_to_json_with_use_standard_json_time_format_config_set_to_false - old, ActiveSupport.use_standard_json_time_format = ActiveSupport.use_standard_json_time_format, false - assert_equal "\"1999/12/31 19:00:00 -0500\"", ActiveSupport::JSON.encode(@twz) - ensure - ActiveSupport.use_standard_json_time_format = old - end - - 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.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) @@ -97,6 +79,11 @@ class TimeWithZoneTest < ActiveSupport::TestCase assert_equal '1999-12-31 19:00:00 EST -0500', @twz.strftime('%Y-%m-%d %H:%M:%S %Z %z') end + def test_strftime_with_escaping + assert_equal '%Z %z', @twz.strftime('%%Z %%z') + assert_equal '%EST %-0500', @twz.strftime('%%%Z %%%z') + end + def test_inspect assert_equal 'Fri, 31 Dec 1999 19:00:00 EST -05:00', @twz.inspect end @@ -131,6 +118,10 @@ class TimeWithZoneTest < ActiveSupport::TestCase assert_equal "1999-12-31T19:00:00.001234-05:00", @twz.xmlschema(12) end + def test_xmlschema_with_nil_fractional_seconds + assert_equal "1999-12-31T19:00:00-05:00", @twz.xmlschema(nil) + end + def test_to_yaml assert_match(/^--- 2000-01-01 00:00:00(\.0+)?\s*Z\n/, @twz.to_yaml) end @@ -262,16 +253,31 @@ class TimeWithZoneTest < ActiveSupport::TestCase assert_equal 86_400.0, ActiveSupport::TimeWithZone.new( Time.utc(2000, 1, 2), ActiveSupport::TimeZone['Hawaii'] ) - Time.utc(2000, 1, 1) end + def test_minus_with_time_precision + assert_equal 86_399.999999998, ActiveSupport::TimeWithZone.new( Time.utc(2000, 1, 2, 23, 59, 59, Rational(999999999, 1000)), ActiveSupport::TimeZone['UTC'] ) - Time.utc(2000, 1, 2, 0, 0, 0, Rational(1, 1000)) + assert_equal 86_399.999999998, ActiveSupport::TimeWithZone.new( Time.utc(2000, 1, 2, 23, 59, 59, Rational(999999999, 1000)), ActiveSupport::TimeZone['Hawaii'] ) - Time.utc(2000, 1, 2, 0, 0, 0, Rational(1, 1000)) + end + def test_minus_with_time_with_zone twz1 = ActiveSupport::TimeWithZone.new( Time.utc(2000, 1, 1), ActiveSupport::TimeZone['UTC'] ) twz2 = ActiveSupport::TimeWithZone.new( Time.utc(2000, 1, 2), ActiveSupport::TimeZone['UTC'] ) assert_equal 86_400.0, twz2 - twz1 end + def test_minus_with_time_with_zone_precision + twz1 = ActiveSupport::TimeWithZone.new( Time.utc(2000, 1, 1, 0, 0, 0, Rational(1, 1000)), ActiveSupport::TimeZone['UTC'] ) + twz2 = ActiveSupport::TimeWithZone.new( Time.utc(2000, 1, 1, 23, 59, 59, Rational(999999999, 1000)), ActiveSupport::TimeZone['UTC'] ) + assert_equal 86_399.999999998, twz2 - twz1 + end + def test_minus_with_datetime assert_equal 86_400.0, ActiveSupport::TimeWithZone.new( Time.utc(2000, 1, 2), ActiveSupport::TimeZone['UTC'] ) - DateTime.civil(2000, 1, 1) end + def test_minus_with_datetime_precision + assert_equal 86_399.999999999, ActiveSupport::TimeWithZone.new( Time.utc(2000, 1, 1, 23, 59, 59, Rational(999999999, 1000)), ActiveSupport::TimeZone['UTC'] ) - DateTime.civil(2000, 1, 1) + end + def test_minus_with_wrapped_datetime assert_equal 86_400.0, ActiveSupport::TimeWithZone.new( DateTime.civil(2000, 1, 2), ActiveSupport::TimeZone['UTC'] ) - Time.utc(2000, 1, 1) assert_equal 86_400.0, ActiveSupport::TimeWithZone.new( DateTime.civil(2000, 1, 2), ActiveSupport::TimeZone['UTC'] ) - DateTime.civil(2000, 1, 1) @@ -366,6 +372,7 @@ class TimeWithZoneTest < ActiveSupport::TestCase end def test_acts_like_time + assert @twz.acts_like_time? assert @twz.acts_like?(:time) assert ActiveSupport::TimeWithZone.new(DateTime.civil(2000), @time_zone).acts_like?(:time) end @@ -511,6 +518,16 @@ class TimeWithZoneTest < ActiveSupport::TestCase assert_equal "Fri, 31 Dec 1999 19:00:30 EST -05:00", @twz.change(:sec => 30).inspect end + def test_change_at_dst_boundary + twz = ActiveSupport::TimeWithZone.new(Time.at(1319936400).getutc, ActiveSupport::TimeZone['Madrid']) + assert_equal twz, twz.change(:min => 0) + end + + def test_round_at_dst_boundary + twz = ActiveSupport::TimeWithZone.new(Time.at(1319936400).getutc, ActiveSupport::TimeZone['Madrid']) + assert_equal twz, twz.round + end + def test_advance assert_equal "Fri, 31 Dec 1999 19:00:00 EST -05:00", @twz.inspect assert_equal "Mon, 31 Dec 2001 19:00:00 EST -05:00", @twz.advance(:years => 2).inspect @@ -795,29 +812,25 @@ class TimeWithZoneTest < ActiveSupport::TestCase end def test_no_method_error_has_proper_context + rubinius_skip "Error message inconsistency" + 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 - yield - ensure - old_tz ? ENV['TZ'] = old_tz : ENV.delete('TZ') - end end class TimeWithZoneMethodsForTimeAndDateTimeTest < ActiveSupport::TestCase + include TimeZoneTestHelpers + def setup - @t, @dt = Time.utc(2000), DateTime.civil(2000) + @t, @dt, @zone = Time.utc(2000), DateTime.civil(2000), Time.zone end def teardown - Time.zone = nil + Time.zone = @zone end def test_in_time_zone @@ -873,8 +886,6 @@ class TimeWithZoneMethodsForTimeAndDateTimeTest < ActiveSupport::TestCase def test_localtime Time.zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)'] assert_equal @dt.in_time_zone.localtime, @dt.in_time_zone.utc.to_time.getlocal - ensure - Time.zone = nil end def test_use_zone @@ -913,6 +924,7 @@ class TimeWithZoneMethodsForTimeAndDateTimeTest < ActiveSupport::TestCase end def test_time_zone_getter_and_setter_with_zone_default_set + old_zone_default = Time.zone_default Time.zone_default = ActiveSupport::TimeZone['Alaska'] assert_equal ActiveSupport::TimeZone['Alaska'], Time.zone Time.zone = ActiveSupport::TimeZone['Hawaii'] @@ -920,8 +932,7 @@ class TimeWithZoneMethodsForTimeAndDateTimeTest < ActiveSupport::TestCase Time.zone = nil assert_equal ActiveSupport::TimeZone['Alaska'], Time.zone ensure - Time.zone = nil - Time.zone_default = nil + Time.zone_default = old_zone_default end def test_time_zone_setter_is_thread_safe @@ -993,8 +1004,6 @@ class TimeWithZoneMethodsForTimeAndDateTimeTest < ActiveSupport::TestCase assert_equal 'Eastern Time (US & Canada)', Time.current.time_zone.name assert_equal Time.utc(2000), Time.current.time end - ensure - Time.zone = nil end def test_time_in_time_zone_doesnt_affect_receiver @@ -1005,25 +1014,15 @@ class TimeWithZoneMethodsForTimeAndDateTimeTest < ActiveSupport::TestCase 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 - yield - ensure - old_tz ? ENV['TZ'] = old_tz : ENV.delete('TZ') - end end class TimeWithZoneMethodsForDate < ActiveSupport::TestCase + include TimeZoneTestHelpers + 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 @@ -1056,28 +1055,17 @@ class TimeWithZoneMethodsForDate < ActiveSupport::TestCase 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 + include TimeZoneTestHelpers + 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 @@ -1132,13 +1120,4 @@ class TimeWithZoneMethodsForString < ActiveSupport::TestCase 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/core_ext/uri_ext_test.rb b/activesupport/test/core_ext/uri_ext_test.rb index 03e388dd7a..43a5997ddd 100644 --- a/activesupport/test/core_ext/uri_ext_test.rb +++ b/activesupport/test/core_ext/uri_ext_test.rb @@ -7,7 +7,7 @@ class URIExtTest < ActiveSupport::TestCase def test_uri_decode_handle_multibyte str = "\xE6\x97\xA5\xE6\x9C\xAC\xE8\xAA\x9E" # Ni-ho-nn-go in UTF-8, means Japanese. - parser = URI::Parser.new + parser = URI.parser assert_equal str, parser.unescape(parser.escape(str)) end end diff --git a/activesupport/test/dependencies_test.rb b/activesupport/test/dependencies_test.rb index 00bec5bd9d..702e26859a 100644 --- a/activesupport/test/dependencies_test.rb +++ b/activesupport/test/dependencies_test.rb @@ -16,15 +16,18 @@ module ModuleWithConstant end class DependenciesTest < ActiveSupport::TestCase - def teardown - ActiveSupport::Dependencies.clear + include DependenciesTestHelpers + + setup do + @loaded_features_copy = $LOADED_FEATURES.dup end - include DependenciesTestHelpers + teardown do + ActiveSupport::Dependencies.clear + $LOADED_FEATURES.replace(@loaded_features_copy) + end def test_depend_on_path - skip "LoadError#path does not exist" if RUBY_VERSION < '2.0.0' - expected = assert_raises(LoadError) do Kernel.require 'omgwtfbbq' end @@ -47,24 +50,27 @@ class DependenciesTest < ActiveSupport::TestCase end def test_tracking_loaded_files - require_dependency 'dependencies/service_one' - require_dependency 'dependencies/service_two' - assert_equal 2, ActiveSupport::Dependencies.loaded.size + with_loading do + require_dependency 'dependencies/service_one' + require_dependency 'dependencies/service_two' + assert_equal 2, ActiveSupport::Dependencies.loaded.size + end ensure - Object.send(:remove_const, :ServiceOne) if Object.const_defined?(:ServiceOne) - Object.send(:remove_const, :ServiceTwo) if Object.const_defined?(:ServiceTwo) + remove_constants(:ServiceOne, :ServiceTwo) end def test_tracking_identical_loaded_files - require_dependency 'dependencies/service_one' - require_dependency 'dependencies/service_one' - assert_equal 1, ActiveSupport::Dependencies.loaded.size + with_loading do + require_dependency 'dependencies/service_one' + require_dependency 'dependencies/service_one' + assert_equal 1, ActiveSupport::Dependencies.loaded.size + end ensure - Object.send(:remove_const, :ServiceOne) if Object.const_defined?(:ServiceOne) + remove_constants(:ServiceOne) end def test_missing_dependency_raises_missing_source_file - assert_raise(MissingSourceFile) { require_dependency("missing_service") } + assert_raise(LoadError) { require_dependency("missing_service") } end def test_dependency_which_raises_exception_isnt_added_to_loaded_set @@ -80,8 +86,8 @@ class DependenciesTest < ActiveSupport::TestCase assert_equal 'Loading me failed, so do not add to loaded or history.', e.message assert_equal count + 1, $raises_exception_load_count - assert !ActiveSupport::Dependencies.loaded.include?(filename) - assert !ActiveSupport::Dependencies.history.include?(filename) + assert_not ActiveSupport::Dependencies.loaded.include?(filename) + assert_not ActiveSupport::Dependencies.history.include?(filename) end end end @@ -89,7 +95,6 @@ class DependenciesTest < ActiveSupport::TestCase 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 @@ -97,13 +102,12 @@ class DependenciesTest < ActiveSupport::TestCase 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 - filename = "check_warnings" expanded = File.expand_path("#{File.dirname(__FILE__)}/dependencies/#{filename}") $check_warnings_load_count = 0 - assert !ActiveSupport::Dependencies.loaded.include?(expanded) - assert !ActiveSupport::Dependencies.history.include?(expanded) + assert_not ActiveSupport::Dependencies.loaded.include?(expanded) + assert_not ActiveSupport::Dependencies.history.include?(expanded) silence_warnings { require_dependency filename } assert_equal 1, $check_warnings_load_count @@ -111,7 +115,7 @@ class DependenciesTest < ActiveSupport::TestCase assert ActiveSupport::Dependencies.loaded.include?(expanded) ActiveSupport::Dependencies.clear - assert !ActiveSupport::Dependencies.loaded.include?(expanded) + assert_not ActiveSupport::Dependencies.loaded.include?(expanded) assert ActiveSupport::Dependencies.history.include?(expanded) silence_warnings { require_dependency filename } @@ -120,7 +124,7 @@ class DependenciesTest < ActiveSupport::TestCase assert ActiveSupport::Dependencies.loaded.include?(expanded) ActiveSupport::Dependencies.clear - assert !ActiveSupport::Dependencies.loaded.include?(expanded) + assert_not ActiveSupport::Dependencies.loaded.include?(expanded) assert ActiveSupport::Dependencies.history.include?(expanded) enable_warnings { require_dependency filename } @@ -153,6 +157,31 @@ class DependenciesTest < ActiveSupport::TestCase end end + def test_ensures_the_expected_constant_is_defined + with_autoloading_fixtures do + e = assert_raise(LoadError) { Typo } + assert_match %r{Unable to autoload constant Typo, expected .*activesupport/test/autoloading_fixtures/typo.rb to define it}, e.message + end + end + + def test_require_dependency_does_not_assume_any_particular_constant_is_defined + with_autoloading_fixtures do + require_dependency 'typo' + assert_equal 1, TypO + end + end + + # Regression, see https://github.com/rails/rails/issues/16468. + def test_require_dependency_interaction_with_autoloading + with_autoloading_fixtures do + require_dependency 'typo' + assert_equal 1, TypO + + e = assert_raise(LoadError) { Typo } + assert_match %r{Unable to autoload constant Typo, expected .*activesupport/test/autoloading_fixtures/typo.rb to define it}, e.message + end + end + def test_module_loading with_autoloading_fixtures do assert_kind_of Module, A @@ -174,43 +203,49 @@ class DependenciesTest < ActiveSupport::TestCase def test_directories_manifest_as_modules_unless_const_defined with_autoloading_fixtures do assert_kind_of Module, ModuleFolder - Object.__send__ :remove_const, :ModuleFolder end + ensure + remove_constants(:ModuleFolder) end def test_module_with_nested_class with_autoloading_fixtures do assert_kind_of Class, ModuleFolder::NestedClass - Object.__send__ :remove_const, :ModuleFolder end + ensure + remove_constants(:ModuleFolder) end def test_module_with_nested_inline_class with_autoloading_fixtures do assert_kind_of Class, ModuleFolder::InlineClass - Object.__send__ :remove_const, :ModuleFolder end + ensure + remove_constants(:ModuleFolder) end def test_directories_may_manifest_as_nested_classes with_autoloading_fixtures do assert_kind_of Class, ClassFolder - Object.__send__ :remove_const, :ClassFolder end + ensure + remove_constants(:ClassFolder) end def test_class_with_nested_class with_autoloading_fixtures do assert_kind_of Class, ClassFolder::NestedClass - Object.__send__ :remove_const, :ClassFolder end + ensure + remove_constants(:ClassFolder) end def test_class_with_nested_inline_class with_autoloading_fixtures do assert_kind_of Class, ClassFolder::InlineClass - Object.__send__ :remove_const, :ClassFolder end + ensure + remove_constants(:ClassFolder) end def test_class_with_nested_inline_subclass_of_parent @@ -218,8 +253,9 @@ class DependenciesTest < ActiveSupport::TestCase assert_kind_of Class, ClassFolder::ClassFolderSubclass assert_kind_of Class, ClassFolder assert_equal 'indeed', ClassFolder::ClassFolderSubclass::ConstantInClassFolder - Object.__send__ :remove_const, :ClassFolder end + ensure + remove_constants(:ClassFolder) end def test_nested_class_can_access_sibling @@ -227,16 +263,15 @@ class DependenciesTest < ActiveSupport::TestCase sibling = ModuleFolder::NestedClass.class_eval "NestedSibling" assert defined?(ModuleFolder::NestedSibling) assert_equal ModuleFolder::NestedSibling, sibling - Object.__send__ :remove_const, :ModuleFolder end + ensure + remove_constants(:ModuleFolder) end def test_doesnt_break_normal_require path = File.expand_path("../autoloading_fixtures/load_path", __FILE__) original_path = $:.dup - original_features = $".dup $:.push(path) - with_autoloading_fixtures do # The _ = assignments are to prevent warnings _ = RequiresConstant @@ -248,15 +283,13 @@ class DependenciesTest < ActiveSupport::TestCase assert defined?(LoadedConstant) end ensure - remove_constants(:RequiresConstant, :LoadedConstant, :LoadsConstant) - $".replace(original_features) + remove_constants(:RequiresConstant, :LoadedConstant) $:.replace(original_path) end def test_doesnt_break_normal_require_nested path = File.expand_path("../autoloading_fixtures/load_path", __FILE__) original_path = $:.dup - original_features = $".dup $:.push(path) with_autoloading_fixtures do @@ -271,14 +304,12 @@ class DependenciesTest < ActiveSupport::TestCase end ensure remove_constants(:RequiresConstant, :LoadedConstant, :LoadsConstant) - $".replace(original_features) $:.replace(original_path) end def test_require_returns_true_when_file_not_yet_required path = File.expand_path("../autoloading_fixtures/load_path", __FILE__) original_path = $:.dup - original_features = $".dup $:.push(path) with_loading do @@ -286,14 +317,12 @@ class DependenciesTest < ActiveSupport::TestCase end ensure remove_constants(:LoadedConstant) - $".replace(original_features) $:.replace(original_path) end def test_require_returns_true_when_file_not_yet_required_even_when_no_new_constants_added path = File.expand_path("../autoloading_fixtures/load_path", __FILE__) original_path = $:.dup - original_features = $".dup $:.push(path) with_loading do @@ -302,14 +331,12 @@ class DependenciesTest < ActiveSupport::TestCase end ensure remove_constants(:LoadedConstant) - $".replace(original_features) $:.replace(original_path) end def test_require_returns_false_when_file_already_required path = File.expand_path("../autoloading_fixtures/load_path", __FILE__) original_path = $:.dup - original_features = $".dup $:.push(path) with_loading do @@ -318,7 +345,6 @@ class DependenciesTest < ActiveSupport::TestCase end ensure remove_constants(:LoadedConstant) - $".replace(original_features) $:.replace(original_path) end @@ -326,14 +352,11 @@ class DependenciesTest < ActiveSupport::TestCase with_loading do assert_raise(LoadError) { require 'this_file_dont_exist_dude' } end - ensure - remove_constants(:LoadedConstant) end def test_load_returns_true_when_file_found path = File.expand_path("../autoloading_fixtures/load_path", __FILE__) original_path = $:.dup - original_features = $".dup $:.push(path) with_loading do @@ -342,7 +365,6 @@ class DependenciesTest < ActiveSupport::TestCase end ensure remove_constants(:LoadedConstant) - $".replace(original_features) $:.replace(original_path) end @@ -350,27 +372,30 @@ class DependenciesTest < ActiveSupport::TestCase with_loading do assert_raise(LoadError) { load 'this_file_dont_exist_dude.rb' } end - ensure - remove_constants(:LoadedConstant) end def failing_test_access_thru_and_upwards_fails with_autoloading_fixtures do - assert ! defined?(ModuleFolder) + assert_not defined?(ModuleFolder) assert_raise(NameError) { ModuleFolder::Object } assert_raise(NameError) { ModuleFolder::NestedClass::Object } - Object.__send__ :remove_const, :ModuleFolder end + ensure + remove_constants(:ModuleFolder) end def test_non_existing_const_raises_name_error_with_fully_qualified_name with_autoloading_fixtures do e = assert_raise(NameError) { A::DoesNotExist.nil? } assert_equal "uninitialized constant A::DoesNotExist", e.message + assert_equal :DoesNotExist, e.name e = assert_raise(NameError) { A::B::DoesNotExist.nil? } assert_equal "uninitialized constant A::B::DoesNotExist", e.message + assert_equal :DoesNotExist, e.name end + ensure + remove_constants(:A) end def test_smart_name_error_strings @@ -460,9 +485,9 @@ class DependenciesTest < ActiveSupport::TestCase nil_name = Module.new def nil_name.name() nil end assert !ActiveSupport::Dependencies.autoloaded?(nil_name) - - Object.class_eval { remove_const :ModuleFolder } end + ensure + remove_constants(:ModuleFolder) end def test_qualified_name_for @@ -520,6 +545,8 @@ class DependenciesTest < ActiveSupport::TestCase assert_kind_of Module, ::ModuleWithCustomConstMissing::A assert_kind_of String, ::ModuleWithCustomConstMissing::A::B end + ensure + remove_constants(:ModuleWithCustomConstMissing) end def test_const_missing_in_anonymous_modules_loads_top_level_constants @@ -528,6 +555,8 @@ class DependenciesTest < ActiveSupport::TestCase klass = Class.new.class_eval "E" assert_equal E, klass end + ensure + remove_constants(:E) end def test_const_missing_in_anonymous_modules_raises_if_the_constant_belongs_to_Object @@ -537,19 +566,24 @@ class DependenciesTest < ActiveSupport::TestCase mod = Module.new e = assert_raise(NameError) { mod::E } assert_equal 'E cannot be autoloaded from an anonymous class or module', e.message + assert_equal :E, e.name end + ensure + remove_constants(:E) end def test_removal_from_tree_should_be_detected with_loading 'dependencies' do c = ServiceOne ActiveSupport::Dependencies.clear - assert ! defined?(ServiceOne) + assert_not defined?(ServiceOne) e = assert_raise ArgumentError do ActiveSupport::Dependencies.load_missing_constant(c, :FakeMissing) end assert_match %r{ServiceOne has been removed from the module tree}i, e.message end + ensure + remove_constants(:ServiceOne) end def test_references_should_work @@ -558,42 +592,46 @@ class DependenciesTest < ActiveSupport::TestCase service_one_first = ServiceOne assert_equal service_one_first, c.get("ServiceOne") ActiveSupport::Dependencies.clear - assert ! defined?(ServiceOne) - + assert_not defined?(ServiceOne) service_one_second = ServiceOne assert_not_equal service_one_first, c.get("ServiceOne") assert_equal service_one_second, c.get("ServiceOne") end + ensure + remove_constants(:ServiceOne) end def test_constantize_shortcut_for_cached_constant_lookups with_loading 'dependencies' do assert_equal ServiceOne, ActiveSupport::Dependencies.constantize("ServiceOne") end + ensure + remove_constants(:ServiceOne) end def test_nested_load_error_isnt_rescued with_loading 'dependencies' do - assert_raise(MissingSourceFile) do + assert_raise(LoadError) do RequiresNonexistent1 end end end def test_autoload_once_paths_do_not_add_to_autoloaded_constants + old_path = ActiveSupport::Dependencies.autoload_once_paths with_autoloading_fixtures do ActiveSupport::Dependencies.autoload_once_paths = ActiveSupport::Dependencies.autoload_paths.dup - assert ! ActiveSupport::Dependencies.autoloaded?("ModuleFolder") - assert ! ActiveSupport::Dependencies.autoloaded?("ModuleFolder::NestedClass") - assert ! ActiveSupport::Dependencies.autoloaded?(ModuleFolder) + assert_not ActiveSupport::Dependencies.autoloaded?("ModuleFolder") + assert_not ActiveSupport::Dependencies.autoloaded?("ModuleFolder::NestedClass") + assert_not ActiveSupport::Dependencies.autoloaded?(ModuleFolder) 1 if ModuleFolder::NestedClass # 1 if to avoid warning - assert ! ActiveSupport::Dependencies.autoloaded?(ModuleFolder::NestedClass) + assert_not ActiveSupport::Dependencies.autoloaded?(ModuleFolder::NestedClass) end ensure - Object.class_eval { remove_const :ModuleFolder } - ActiveSupport::Dependencies.autoload_once_paths = [] + remove_constants(:ModuleFolder) + ActiveSupport::Dependencies.autoload_once_paths = old_path end def test_autoload_once_pathnames_do_not_add_to_autoloaded_constants @@ -602,15 +640,15 @@ class DependenciesTest < ActiveSupport::TestCase ActiveSupport::Dependencies.autoload_paths = pathnames ActiveSupport::Dependencies.autoload_once_paths = pathnames - assert ! ActiveSupport::Dependencies.autoloaded?("ModuleFolder") - assert ! ActiveSupport::Dependencies.autoloaded?("ModuleFolder::NestedClass") - assert ! ActiveSupport::Dependencies.autoloaded?(ModuleFolder) + assert_not ActiveSupport::Dependencies.autoloaded?("ModuleFolder") + assert_not ActiveSupport::Dependencies.autoloaded?("ModuleFolder::NestedClass") + assert_not ActiveSupport::Dependencies.autoloaded?(ModuleFolder) 1 if ModuleFolder::NestedClass # 1 if to avoid warning - assert ! ActiveSupport::Dependencies.autoloaded?(ModuleFolder::NestedClass) + assert_not ActiveSupport::Dependencies.autoloaded?(ModuleFolder::NestedClass) end ensure - Object.class_eval { remove_const :ModuleFolder } + remove_constants(:ModuleFolder) ActiveSupport::Dependencies.autoload_once_paths = [] end @@ -620,6 +658,8 @@ class DependenciesTest < ActiveSupport::TestCase assert_equal 10, ApplicationController assert ActiveSupport::Dependencies.autoloaded?(:ApplicationController) end + ensure + remove_constants(:ApplicationController) end def test_preexisting_constants_are_not_marked_as_autoloaded @@ -635,9 +675,8 @@ class DependenciesTest < ActiveSupport::TestCase assert ! ActiveSupport::Dependencies.autoloaded?(:E), "E shouldn't be marked autoloaded!" ActiveSupport::Dependencies.clear end - ensure - Object.class_eval { remove_const :E } + remove_constants(:E) end def test_constants_in_capitalized_nesting_marked_as_autoloaded @@ -646,6 +685,8 @@ class DependenciesTest < ActiveSupport::TestCase assert ActiveSupport::Dependencies.autoloaded?("HTML::SomeClass") end + ensure + remove_constants(:HTML) end def test_unloadable @@ -676,7 +717,7 @@ class DependenciesTest < ActiveSupport::TestCase assert_equal false, M.unloadable end ensure - Object.class_eval { remove_const :M } + remove_constants(:M) end def test_unloadable_constants_should_receive_callback @@ -687,7 +728,7 @@ class DependenciesTest < ActiveSupport::TestCase ActiveSupport::Dependencies.clear assert !defined?(C) ensure - Object.class_eval { remove_const :C } if defined?(C) + remove_constants(:C) end def test_new_contants_in_without_constants @@ -701,7 +742,7 @@ class DependenciesTest < ActiveSupport::TestCase }.map(&:to_s) assert ActiveSupport::Dependencies.constant_watch_stack.all? {|k,v| v.empty? } ensure - Object.class_eval { remove_const :Hello } + remove_constants(:Hello) end def test_new_constants_in_with_nesting @@ -718,9 +759,7 @@ class DependenciesTest < ActiveSupport::TestCase assert_equal ["OuterAfter", "OuterBefore"], outer.sort.map(&:to_s) assert ActiveSupport::Dependencies.constant_watch_stack.all? {|k,v| v.empty? } ensure - %w(OuterBefore Inner OuterAfter).each do |name| - Object.class_eval { remove_const name if const_defined?(name) } - end + remove_constants(:OuterBefore, :Inner, :OuterAfter) end def test_new_constants_in_module @@ -739,7 +778,7 @@ class DependenciesTest < ActiveSupport::TestCase assert_equal ["M::OuterAfter", "M::OuterBefore"], outer.sort assert ActiveSupport::Dependencies.constant_watch_stack.all? {|k,v| v.empty? } ensure - Object.class_eval { remove_const :M } + remove_constants(:M) end def test_new_constants_in_module_using_name @@ -757,7 +796,7 @@ class DependenciesTest < ActiveSupport::TestCase assert_equal ["M::OuterAfter", "M::OuterBefore"], outer.sort assert ActiveSupport::Dependencies.constant_watch_stack.all? {|k,v| v.empty? } ensure - Object.class_eval { remove_const :M } + remove_constants(:M) end def test_new_constants_in_with_inherited_constants @@ -775,26 +814,27 @@ class DependenciesTest < ActiveSupport::TestCase def test_file_with_multiple_constants_and_require_dependency with_autoloading_fixtures do - assert ! defined?(MultipleConstantFile) - assert ! defined?(SiblingConstant) + assert_not defined?(MultipleConstantFile) + assert_not defined?(SiblingConstant) require_dependency 'multiple_constant_file' assert defined?(MultipleConstantFile) assert defined?(SiblingConstant) assert ActiveSupport::Dependencies.autoloaded?(:MultipleConstantFile) assert ActiveSupport::Dependencies.autoloaded?(:SiblingConstant) - ActiveSupport::Dependencies.clear - assert ! defined?(MultipleConstantFile) - assert ! defined?(SiblingConstant) + assert_not defined?(MultipleConstantFile) + assert_not defined?(SiblingConstant) end + ensure + remove_constants(:MultipleConstantFile, :SiblingConstant) end def test_file_with_multiple_constants_and_auto_loading with_autoloading_fixtures do - assert ! defined?(MultipleConstantFile) - assert ! defined?(SiblingConstant) + assert_not defined?(MultipleConstantFile) + assert_not defined?(SiblingConstant) assert_equal 10, MultipleConstantFile @@ -805,15 +845,17 @@ class DependenciesTest < ActiveSupport::TestCase ActiveSupport::Dependencies.clear - assert ! defined?(MultipleConstantFile) - assert ! defined?(SiblingConstant) + assert_not defined?(MultipleConstantFile) + assert_not defined?(SiblingConstant) end + ensure + remove_constants(:MultipleConstantFile, :SiblingConstant) end def test_nested_file_with_multiple_constants_and_require_dependency with_autoloading_fixtures do - assert ! defined?(ClassFolder::NestedClass) - assert ! defined?(ClassFolder::SiblingClass) + assert_not defined?(ClassFolder::NestedClass) + assert_not defined?(ClassFolder::SiblingClass) require_dependency 'class_folder/nested_class' @@ -824,15 +866,17 @@ class DependenciesTest < ActiveSupport::TestCase ActiveSupport::Dependencies.clear - assert ! defined?(ClassFolder::NestedClass) - assert ! defined?(ClassFolder::SiblingClass) + assert_not defined?(ClassFolder::NestedClass) + assert_not defined?(ClassFolder::SiblingClass) end + ensure + remove_constants(:ClassFolder) end def test_nested_file_with_multiple_constants_and_auto_loading with_autoloading_fixtures do - assert ! defined?(ClassFolder::NestedClass) - assert ! defined?(ClassFolder::SiblingClass) + assert_not defined?(ClassFolder::NestedClass) + assert_not defined?(ClassFolder::SiblingClass) assert_kind_of Class, ClassFolder::NestedClass @@ -843,9 +887,11 @@ class DependenciesTest < ActiveSupport::TestCase ActiveSupport::Dependencies.clear - assert ! defined?(ClassFolder::NestedClass) - assert ! defined?(ClassFolder::SiblingClass) + assert_not defined?(ClassFolder::NestedClass) + assert_not defined?(ClassFolder::SiblingClass) end + ensure + remove_constants(:ClassFolder) end def test_autoload_doesnt_shadow_no_method_error_with_relative_constant @@ -856,9 +902,8 @@ class DependenciesTest < ActiveSupport::TestCase assert !defined?(::RaisesNoMethodError), "::RaisesNoMethodError is defined but it should have failed!" end end - ensure - Object.class_eval { remove_const :RaisesNoMethodError if const_defined?(:RaisesNoMethodError) } + remove_constants(:RaisesNoMethodError) end def test_autoload_doesnt_shadow_no_method_error_with_absolute_constant @@ -869,9 +914,8 @@ class DependenciesTest < ActiveSupport::TestCase assert !defined?(::RaisesNoMethodError), "::RaisesNoMethodError is defined but it should have failed!" end end - ensure - Object.class_eval { remove_const :RaisesNoMethodError if const_defined?(:RaisesNoMethodError) } + remove_constants(:RaisesNoMethodError) end def test_autoload_doesnt_shadow_error_when_mechanism_not_set_to_load @@ -881,11 +925,12 @@ class DependenciesTest < ActiveSupport::TestCase assert_raise(NameError) { assert_equal 123, ::RaisesNameError::FooBarBaz } end end + ensure + remove_constants(:RaisesNameError) end def test_autoload_doesnt_shadow_name_error with_autoloading_fixtures do - Object.send(:remove_const, :RaisesNameError) if defined?(::RaisesNameError) 2.times do e = assert_raise NameError do ::RaisesNameError::FooBarBaz.object_id @@ -900,19 +945,20 @@ class DependenciesTest < ActiveSupport::TestCase assert !defined?(::RaisesNameError), "::RaisesNameError is defined but it should have failed!" end end - ensure - Object.class_eval { remove_const :RaisesNoMethodError if const_defined?(:RaisesNoMethodError) } + remove_constants(:RaisesNameError) end def test_remove_constant_handles_double_colon_at_start Object.const_set 'DeleteMe', Module.new DeleteMe.const_set 'OrMe', Module.new ActiveSupport::Dependencies.remove_constant "::DeleteMe::OrMe" - assert ! defined?(DeleteMe::OrMe) + assert_not defined?(DeleteMe::OrMe) assert defined?(DeleteMe) ActiveSupport::Dependencies.remove_constant "::DeleteMe" - assert ! defined?(DeleteMe) + assert_not defined?(DeleteMe) + ensure + remove_constants(:DeleteMe) end def test_remove_constant_does_not_trigger_loading_autoloads @@ -922,7 +968,9 @@ class DependenciesTest < ActiveSupport::TestCase end assert_nil ActiveSupport::Dependencies.remove_constant(constant), "Kernel#autoload has been triggered by remove_constant" - assert !defined?(ShouldNotBeAutoloaded) + assert_not defined?(ShouldNotBeAutoloaded) + ensure + remove_constants(constant) end def test_remove_constant_does_not_autoload_already_removed_parents_as_a_side_effect @@ -931,11 +979,14 @@ class DependenciesTest < ActiveSupport::TestCase _ = ::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) + assert_not defined?(A) end + ensure + remove_constants(:A) end def test_load_once_constants_should_not_be_unloaded + old_path = ActiveSupport::Dependencies.autoload_once_paths with_autoloading_fixtures do ActiveSupport::Dependencies.autoload_once_paths = ActiveSupport::Dependencies.autoload_paths _ = ::A # assignment to silence parse-time warning "possibly useless use of :: in void context" @@ -944,30 +995,58 @@ class DependenciesTest < ActiveSupport::TestCase assert defined?(A) end ensure - ActiveSupport::Dependencies.autoload_once_paths = [] - Object.class_eval { remove_const :A if const_defined?(:A) } + ActiveSupport::Dependencies.autoload_once_paths = old_path + remove_constants(:A) + end + + def test_access_unloaded_constants_for_reload + with_autoloading_fixtures do + assert_kind_of Module, A + assert_kind_of Class, A::B # Necessary to load A::B for the test + ActiveSupport::Dependencies.mark_for_unload(A::B) + ActiveSupport::Dependencies.remove_unloadable_constants! + + A::B # Make sure no circular dependency error + end + ensure + remove_constants(:A) end + def test_autoload_once_paths_should_behave_when_recursively_loading + old_path = ActiveSupport::Dependencies.autoload_once_paths with_loading 'dependencies', 'autoloading_fixtures' do ActiveSupport::Dependencies.autoload_once_paths = [ActiveSupport::Dependencies.autoload_paths.last] - assert !defined?(CrossSiteDependency) + assert_not defined?(CrossSiteDependency) assert_nothing_raised { CrossSiteDepender.nil? } assert defined?(CrossSiteDependency) - assert !ActiveSupport::Dependencies.autoloaded?(CrossSiteDependency), + assert_not ActiveSupport::Dependencies.autoloaded?(CrossSiteDependency), "CrossSiteDependency shouldn't be marked as autoloaded!" ActiveSupport::Dependencies.clear assert defined?(CrossSiteDependency), "CrossSiteDependency shouldn't have been unloaded!" end ensure - ActiveSupport::Dependencies.autoload_once_paths = [] + ActiveSupport::Dependencies.autoload_once_paths = old_path + remove_constants(:CrossSiteDependency) end def test_hook_called_multiple_times assert_nothing_raised { ActiveSupport::Dependencies.hook! } end + def test_load_and_require_stay_private + assert Object.private_methods.include?(:load) + assert Object.private_methods.include?(:require) + + ActiveSupport::Dependencies.unhook! + + assert Object.private_methods.include?(:load) + assert Object.private_methods.include?(:require) + ensure + ActiveSupport::Dependencies.hook! + end + def test_unhook ActiveSupport::Dependencies.unhook! assert !Module.new.respond_to?(:const_missing_without_dependencies) @@ -975,11 +1054,4 @@ class DependenciesTest < ActiveSupport::TestCase ensure ActiveSupport::Dependencies.hook! end - -private - def remove_constants(*constants) - constants.each do |constant| - Object.send(:remove_const, constant) if Object.const_defined?(constant) - end - end end diff --git a/activesupport/test/dependencies_test_helpers.rb b/activesupport/test/dependencies_test_helpers.rb index 9268512a97..e4d5197112 100644 --- a/activesupport/test/dependencies_test_helpers.rb +++ b/activesupport/test/dependencies_test_helpers.rb @@ -13,6 +13,7 @@ module DependenciesTestHelpers ActiveSupport::Dependencies.autoload_paths = prior_autoload_paths ActiveSupport::Dependencies.mechanism = old_mechanism ActiveSupport::Dependencies.explicitly_unloadable_constants = [] + ActiveSupport::Dependencies.clear end def with_autoloading_fixtures(&block) diff --git a/activesupport/test/deprecation_test.rb b/activesupport/test/deprecation_test.rb index ee1c69502e..7aff56cbad 100644 --- a/activesupport/test/deprecation_test.rb +++ b/activesupport/test/deprecation_test.rb @@ -355,4 +355,21 @@ class DeprecationTest < ActiveSupport::TestCase end deprecator end + + def capture(stream) + stream = stream.to_s + captured_stream = Tempfile.new(stream) + stream_io = eval("$#{stream}") + origin_stream = stream_io.dup + stream_io.reopen(captured_stream) + + yield + + stream_io.rewind + return captured_stream.read + ensure + captured_stream.close + captured_stream.unlink + stream_io.reopen(origin_stream) + end end diff --git a/activesupport/test/hash_with_indifferent_access_test.rb b/activesupport/test/hash_with_indifferent_access_test.rb new file mode 100644 index 0000000000..843994147b --- /dev/null +++ b/activesupport/test/hash_with_indifferent_access_test.rb @@ -0,0 +1,10 @@ +require 'abstract_unit' +require 'active_support/hash_with_indifferent_access' + +class HashWithIndifferentAccessTest < ActiveSupport::TestCase + def test_reverse_merge + hash = HashWithIndifferentAccess.new key: :old_value + hash.reverse_merge! key: :new_value + assert_equal :old_value, hash[:key] + end +end
\ No newline at end of file diff --git a/activesupport/test/i18n_test.rb b/activesupport/test/i18n_test.rb index 5ef59b6e6b..3faa15e7fd 100644 --- a/activesupport/test/i18n_test.rb +++ b/activesupport/test/i18n_test.rb @@ -99,7 +99,6 @@ class I18nTest < ActiveSupport::TestCase end def test_to_sentence_with_empty_i18n_store - I18n.backend.store_translations 'empty', {} assert_equal 'a, b, and c', %w[a b c].to_sentence(locale: 'empty') end end diff --git a/activesupport/test/inflector_test.rb b/activesupport/test/inflector_test.rb index 35967ba656..be68bb2e2e 100644 --- a/activesupport/test/inflector_test.rb +++ b/activesupport/test/inflector_test.rb @@ -120,10 +120,14 @@ class InflectorTest < ActiveSupport::TestCase ["SSLError", "ssl_error", "SSL error", "SSL Error"], ["RESTful", "restful", "RESTful", "RESTful"], ["RESTfulController", "restful_controller", "RESTful controller", "RESTful Controller"], + ["Nested::RESTful", "nested/restful", "Nested/RESTful", "Nested/RESTful"], ["IHeartW3C", "i_heart_w3c", "I heart W3C", "I Heart W3C"], ["PhDRequired", "phd_required", "PhD required", "PhD Required"], ["IRoRU", "i_ror_u", "I RoR u", "I RoR U"], ["RESTfulHTTPAPI", "restful_http_api", "RESTful HTTP API", "RESTful HTTP API"], + ["HTTP::RESTful", "http/restful", "HTTP/RESTful", "HTTP/RESTful"], + ["HTTP::RESTfulAPI", "http/restful_api", "HTTP/RESTful API", "HTTP/RESTful API"], + ["APIRESTful", "api_restful", "API RESTful", "API RESTful"], # misdirection ["Capistrano", "capistrano", "Capistrano", "Capistrano"], @@ -200,6 +204,7 @@ class InflectorTest < ActiveSupport::TestCase def test_demodulize assert_equal "Account", ActiveSupport::Inflector.demodulize("MyApplication::Billing::Account") assert_equal "Account", ActiveSupport::Inflector.demodulize("Account") + assert_equal "Account", ActiveSupport::Inflector.demodulize("::Account") assert_equal "", ActiveSupport::Inflector.demodulize("") end @@ -485,8 +490,8 @@ class InflectorTest < ActiveSupport::TestCase assert_equal [], inflect.uncountables # restore all the inflections - singulars.reverse.each { |singular| inflect.singular(*singular) } - plurals.reverse.each { |plural| inflect.plural(*plural) } + singulars.reverse_each { |singular| inflect.singular(*singular) } + plurals.reverse_each { |plural| inflect.plural(*plural) } inflect.uncountable(uncountables) assert_equal singulars, inflect.singulars @@ -497,10 +502,10 @@ class InflectorTest < ActiveSupport::TestCase end %w(plurals singulars uncountables humans acronyms).each do |scope| - ActiveSupport::Inflector.inflections do |inflect| - define_method("test_clear_inflections_with_#{scope}") do - with_dup do - # clear the inflections + define_method("test_clear_inflections_with_#{scope}") do + with_dup do + # clear the inflections + ActiveSupport::Inflector.inflections do |inflect| inflect.clear(scope) assert_equal [], inflect.send(scope) end @@ -508,6 +513,14 @@ class InflectorTest < ActiveSupport::TestCase end end + def test_inflections_with_uncountable_words + ActiveSupport::Inflector.inflections do |inflect| + inflect.uncountable "HTTP" + end + + assert_equal "HTTP", ActiveSupport::Inflector.pluralize("HTTP") + end + # Dups the singleton and yields, restoring the original inflections later. # Use this in tests what modify the state of the singleton. # @@ -515,9 +528,10 @@ class InflectorTest < ActiveSupport::TestCase # there are module functions that access ActiveSupport::Inflector.inflections, # so we need to replace the singleton itself. def with_dup - original = ActiveSupport::Inflector::Inflections.instance_variable_get(:@__instance__) - ActiveSupport::Inflector::Inflections.instance_variable_set(:@__instance__, original.dup) + original = ActiveSupport::Inflector::Inflections.instance_variable_get(:@__instance__)[:en] + ActiveSupport::Inflector::Inflections.instance_variable_set(:@__instance__, en: original.dup) + yield ensure - ActiveSupport::Inflector::Inflections.instance_variable_set(:@__instance__, original) + ActiveSupport::Inflector::Inflections.instance_variable_set(:@__instance__, en: original) end end diff --git a/activesupport/test/inflector_test_cases.rb b/activesupport/test/inflector_test_cases.rb index 4bd1b2e47c..3770f00fe3 100644 --- a/activesupport/test/inflector_test_cases.rb +++ b/activesupport/test/inflector_test_cases.rb @@ -141,6 +141,7 @@ module InflectorTestCases "HTMLTidyGenerator" => "html_tidy_generator", "FreeBSD" => "free_bsd", "HTML" => "html", + "ForceXMLController" => "force_xml_controller", } CamelWithModuleToUnderscoreWithSlash = { @@ -208,9 +209,11 @@ module InflectorTestCases } UnderscoreToHuman = { - "employee_salary" => "Employee salary", - "employee_id" => "Employee", - "underground" => "Underground" + 'employee_salary' => 'Employee salary', + 'employee_id' => 'Employee', + 'underground' => 'Underground', + '_id' => 'Id', + '_external_id' => 'External' } UnderscoreToHumanWithoutCapitalize = { @@ -314,7 +317,7 @@ module InflectorTestCases 'child' => 'children', 'sex' => 'sexes', 'move' => 'moves', - 'cow' => 'kine', + 'cow' => 'kine', # Test inflections with different starting letters 'zombie' => 'zombies', 'genus' => 'genera' } diff --git a/activesupport/test/json/decoding_test.rb b/activesupport/test/json/decoding_test.rb index 07d7e530ca..80bf255080 100644 --- a/activesupport/test/json/decoding_test.rb +++ b/activesupport/test/json/decoding_test.rb @@ -73,22 +73,20 @@ class TestJSONDecoding < ActiveSupport::TestCase 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}" + with_parse_json_times(true) do + silence_warnings do + assert_equal expected, ActiveSupport::JSON.decode(json), "JSON decoding \ + failed for #{json}" + end end - ActiveSupport.parse_json_times = prev 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 + with_parse_json_times(false) do + expected = {"a" => "2007-01-01 01:12:34 Z"} + assert_equal expected, ActiveSupport::JSON.decode(%({"a": "2007-01-01 01:12:34 Z"})) + end end def test_failed_json_decoding @@ -101,5 +99,15 @@ class TestJSONDecoding < ActiveSupport::TestCase def test_cannot_pass_unsupported_options assert_raise(ArgumentError) { ActiveSupport::JSON.decode("", create_additions: true) } end + + private + + def with_parse_json_times(value) + old_value = ActiveSupport.parse_json_times + ActiveSupport.parse_json_times = value + yield + ensure + ActiveSupport.parse_json_times = old_value + end end diff --git a/activesupport/test/json/encoding_test.rb b/activesupport/test/json/encoding_test.rb index 78cf4819f9..5c5045da1e 100644 --- a/activesupport/test/json/encoding_test.rb +++ b/activesupport/test/json/encoding_test.rb @@ -3,8 +3,12 @@ require 'securerandom' require 'abstract_unit' require 'active_support/core_ext/string/inflections' require 'active_support/json' +require 'active_support/time' +require 'time_zone_test_helpers' class TestJSONEncoding < ActiveSupport::TestCase + include TimeZoneTestHelpers + class Foo def initialize(a, b) @a, @b = a, b @@ -64,7 +68,7 @@ class TestJSONEncoding < ActiveSupport::TestCase [ 1.0/0.0, %(null) ], [ -1.0/0.0, %(null) ], [ BigDecimal('0.0')/BigDecimal('0.0'), %(null) ], - [ BigDecimal('2.5'), %("#{BigDecimal('2.5').to_s}") ]] + [ BigDecimal('2.5'), %("#{BigDecimal('2.5')}") ]] StringTests = [[ 'this is the <string>', %("this is the \\u003cstring\\u003e")], [ 'a "string" with quotes & an ampersand', %("a \\"string\\" with quotes \\u0026 an ampersand") ], @@ -172,46 +176,6 @@ class TestJSONEncoding < ActiveSupport::TestCase assert_equal "𐒑", decoded_hash['string'] end - def test_reading_encode_big_decimal_as_string_option - assert_deprecated do - assert ActiveSupport.encode_big_decimal_as_string - end - end - - def test_setting_deprecated_encode_big_decimal_as_string_option - assert_raise(NotImplementedError) do - ActiveSupport.encode_big_decimal_as_string = true - end - - assert_raise(NotImplementedError) do - ActiveSupport.encode_big_decimal_as_string = false - end - end - - def test_exception_raised_when_encoding_circular_reference_in_array - a = [1] - a << a - assert_deprecated do - assert_raise(ActiveSupport::JSON::Encoding::CircularReferenceError) { ActiveSupport::JSON.encode(a) } - end - end - - def test_exception_raised_when_encoding_circular_reference_in_hash - a = { :name => 'foo' } - a[:next] = a - assert_deprecated do - assert_raise(ActiveSupport::JSON::Encoding::CircularReferenceError) { ActiveSupport::JSON.encode(a) } - end - end - - def test_exception_raised_when_encoding_circular_reference_in_hash_inside_array - a = { :name => 'foo', :sub => [] } - a[:sub] << a - assert_deprecated do - assert_raise(ActiveSupport::JSON::Encoding::CircularReferenceError) { ActiveSupport::JSON.encode(a) } - end - end - def test_hash_key_identifiers_are_always_quoted values = {0 => 0, 1 => 1, :_ => :_, "$" => "$", "a" => "a", :A => :A, :A0 => :A0, "A0B" => "A0B"} assert_equal %w( "$" "A" "A0" "A0B" "_" "a" "0" "1" ).sort, object_keys(ActiveSupport::JSON.encode(values)) @@ -226,21 +190,17 @@ 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.000-05:00"), ActiveSupport::JSON.encode(Time.local(2005,2,1,15,15,10)) + with_standard_json_time_format(true) do + with_env_tz 'US/Eastern' do + assert_equal %("2005-02-01T15:15:10.000-05:00"), ActiveSupport::JSON.encode(Time.local(2005,2,1,15,15,10)) + end end - ensure - 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 + with_standard_json_time_format(false) do + assert_equal '{"time":"2009/01/01 00:00:00 +0000"}', { :time => Time.utc(2009) }.to_json + end end def test_nested_hash_with_float @@ -330,12 +290,39 @@ class TestJSONEncoding < ActiveSupport::TestCase assert_equal(%([{"address":{"city":"London"}},{"address":{"city":"Paris"}}]), json) end - def test_enumerable_should_pass_encoding_options_to_children_in_as_json - people = [ - { :name => 'John', :address => { :city => 'London', :country => 'UK' }}, - { :name => 'Jean', :address => { :city => 'Paris' , :country => 'France' }} + People = Class.new(BasicObject) do + include Enumerable + def initialize() + @people = [ + { :name => 'John', :address => { :city => 'London', :country => 'UK' }}, + { :name => 'Jean', :address => { :city => 'Paris' , :country => 'France' }} + ] + end + def each(*, &blk) + @people.each do |p| + yield p if blk + p + end.each + end + end + + def test_enumerable_should_generate_json_with_as_json + json = People.new.as_json :only => [:address, :city] + expected = [ + { 'address' => { 'city' => 'London' }}, + { 'address' => { 'city' => 'Paris' }} ] - json = people.each.as_json :only => [:address, :city] + + assert_equal(expected, json) + end + + def test_enumerable_should_generate_json_with_to_json + json = People.new.to_json :only => [:address, :city] + assert_equal(%([{"address":{"city":"London"}},{"address":{"city":"Paris"}}]), json) + end + + def test_enumerable_should_pass_encoding_options_to_children_in_as_json + json = People.new.each.as_json :only => [:address, :city] expected = [ { 'address' => { 'city' => 'London' }}, { 'address' => { 'city' => 'Paris' }} @@ -345,11 +332,7 @@ class TestJSONEncoding < ActiveSupport::TestCase end def test_enumerable_should_pass_encoding_options_to_children_in_to_json - people = [ - { :name => 'John', :address => { :city => 'London', :country => 'UK' }}, - { :name => 'Jean', :address => { :city => 'Paris' , :country => 'France' }} - ] - json = people.each.to_json :only => [:address, :city] + json = People.new.each.to_json :only => [:address, :city] assert_equal(%([{"address":{"city":"London"}},{"address":{"city":"Paris"}}]), json) end @@ -453,16 +436,72 @@ EXPECTED assert_nil h.as_json_called end + def test_twz_to_json_with_use_standard_json_time_format_config_set_to_false + with_standard_json_time_format(false) do + zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)'] + time = ActiveSupport::TimeWithZone.new(Time.utc(2000), zone) + assert_equal "\"1999/12/31 19:00:00 -0500\"", ActiveSupport::JSON.encode(time) + end + end + + def test_twz_to_json_with_use_standard_json_time_format_config_set_to_true + with_standard_json_time_format(true) do + zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)'] + time = ActiveSupport::TimeWithZone.new(Time.utc(2000), zone) + assert_equal "\"1999-12-31T19:00:00.000-05:00\"", ActiveSupport::JSON.encode(time) + end + end + + def test_twz_to_json_with_custom_time_precision + with_standard_json_time_format(true) do + with_time_precision(0) do + zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)'] + time = ActiveSupport::TimeWithZone.new(Time.utc(2000), zone) + assert_equal "\"1999-12-31T19:00:00-05:00\"", ActiveSupport::JSON.encode(time) + end + end + end + + def test_time_to_json_with_custom_time_precision + with_standard_json_time_format(true) do + with_time_precision(0) do + assert_equal "\"2000-01-01T00:00:00Z\"", ActiveSupport::JSON.encode(Time.utc(2000)) + end + end + end + + def test_datetime_to_json_with_custom_time_precision + with_standard_json_time_format(true) do + with_time_precision(0) do + assert_equal "\"2000-01-01T00:00:00+00:00\"", ActiveSupport::JSON.encode(DateTime.new(2000)) + end + end + end + + def test_twz_to_json_when_wrapping_a_date_time + zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)'] + time = ActiveSupport::TimeWithZone.new(DateTime.new(2000), zone) + assert_equal '"1999-12-31T19:00:00.000-05:00"', ActiveSupport::JSON.encode(time) + end + protected def object_keys(json_object) json_object[1..-2].scan(/([^{}:,\s]+):/).flatten.sort end - def with_env_tz(new_tz = 'US/Eastern') - old_tz, ENV['TZ'] = ENV['TZ'], new_tz + def with_standard_json_time_format(boolean = true) + old, ActiveSupport.use_standard_json_time_format = ActiveSupport.use_standard_json_time_format, boolean + yield + ensure + ActiveSupport.use_standard_json_time_format = old + end + + def with_time_precision(value) + old_value = ActiveSupport::JSON::Encoding.time_precision + ActiveSupport::JSON::Encoding.time_precision = value yield ensure - old_tz ? ENV['TZ'] = old_tz : ENV.delete('TZ') + ActiveSupport::JSON::Encoding.time_precision = old_value end end diff --git a/activesupport/test/key_generator_test.rb b/activesupport/test/key_generator_test.rb index 525082d313..f7e8e9a795 100644 --- a/activesupport/test/key_generator_test.rb +++ b/activesupport/test/key_generator_test.rb @@ -29,4 +29,34 @@ class KeyGeneratorTest < ActiveSupport::TestCase end end +class CachingKeyGeneratorTest < ActiveSupport::TestCase + def setup + @secret = SecureRandom.hex(64) + @generator = ActiveSupport::KeyGenerator.new(@secret, :iterations=>2) + @caching_generator = ActiveSupport::CachingKeyGenerator.new(@generator) + end + + test "Generating a cached key for same salt and key size" do + derived_key = @caching_generator.generate_key("some_salt", 32) + cached_key = @caching_generator.generate_key("some_salt", 32) + + assert_equal derived_key, cached_key + assert_equal derived_key.object_id, cached_key.object_id + end + + test "Does not cache key for different salt" do + derived_key = @caching_generator.generate_key("some_salt", 32) + different_salt_key = @caching_generator.generate_key("other_salt", 32) + + assert_not_equal derived_key, different_salt_key + end + + test "Does not cache key for different length" do + derived_key = @caching_generator.generate_key("some_salt", 32) + different_length_key = @caching_generator.generate_key("some_salt", 64) + + assert_not_equal derived_key, different_length_key + end +end + end diff --git a/activesupport/test/message_encryptor_test.rb b/activesupport/test/message_encryptor_test.rb index b6c0a08b05..eb71369397 100644 --- a/activesupport/test/message_encryptor_test.rb +++ b/activesupport/test/message_encryptor_test.rb @@ -1,12 +1,5 @@ require 'abstract_unit' - -begin - require 'openssl' - OpenSSL::Digest::SHA1 -rescue LoadError, NameError - $stderr.puts "Skipping MessageEncryptor test: broken OpenSSL install" -else - +require 'openssl' require 'active_support/time' require 'active_support/json' @@ -97,5 +90,3 @@ class MessageEncryptorTest < ActiveSupport::TestCase ::Base64.strict_encode64(bits) end end - -end diff --git a/activesupport/test/message_verifier_test.rb b/activesupport/test/message_verifier_test.rb index a5748d28ba..6c3519df9a 100644 --- a/activesupport/test/message_verifier_test.rb +++ b/activesupport/test/message_verifier_test.rb @@ -1,12 +1,5 @@ require 'abstract_unit' - -begin - require 'openssl' - OpenSSL::Digest::SHA1 -rescue LoadError, NameError - $stderr.puts "Skipping MessageVerifier test: broken OpenSSL install" -else - +require 'openssl' require 'active_support/time' require 'active_support/json' @@ -27,21 +20,29 @@ class MessageVerifierTest < ActiveSupport::TestCase @data = { :some => "data", :now => Time.local(2010) } end + def test_valid_message + data, hash = @verifier.generate(@data).split("--") + assert !@verifier.valid_message?(nil) + assert !@verifier.valid_message?("") + assert !@verifier.valid_message?("#{data.reverse}--#{hash}") + assert !@verifier.valid_message?("#{data}--#{hash.reverse}") + assert !@verifier.valid_message?("purejunk") + end + def test_simple_round_tripping message = @verifier.generate(@data) + assert_equal @data, @verifier.verified(message) assert_equal @data, @verifier.verify(message) end - def test_missing_signature_raises - assert_not_verified(nil) - assert_not_verified("") + def test_verified_returns_false_on_invalid_message + assert !@verifier.verified("purejunk") end - def test_tampered_data_raises - data, hash = @verifier.generate(@data).split("--") - assert_not_verified("#{data.reverse}--#{hash}") - assert_not_verified("#{data}--#{hash.reverse}") - assert_not_verified("purejunk") + def test_verify_exception_on_invalid_message + assert_raise(ActiveSupport::MessageVerifier::InvalidSignature) do + @verifier.verify("purejunk") + end end def test_alternative_serialization_method @@ -50,6 +51,7 @@ class MessageVerifierTest < ActiveSupport::TestCase verifier = ActiveSupport::MessageVerifier.new("Hey, I'm a secret!", :serializer => JSONSerializer.new) message = verifier.generate({ :foo => 123, 'bar' => Time.utc(2010) }) exp = { "foo" => 123, "bar" => "2010-01-01T00:00:00.000Z" } + assert_equal exp, verifier.verified(message) assert_equal exp, verifier.verify(message) ensure ActiveSupport.use_standard_json_time_format = prev @@ -63,17 +65,21 @@ class MessageVerifierTest < ActiveSupport::TestCase # valid_message = "BAh7BjoIZm9vbzonTWVzc2FnZVZlcmlmaWVyVGVzdDo6QXV0b2xvYWRDbGFzcwY6CUBmb29JIghmb28GOgZFVA==--f3ef39a5241c365083770566dc7a9eb5d6ace914" exception = assert_raise(ArgumentError, NameError) do + @verifier.verified(valid_message) + end + assert_includes ["uninitialized constant MessageVerifierTest::AutoloadClass", + "undefined class/module MessageVerifierTest::AutoloadClass"], exception.message + exception = assert_raise(ArgumentError, NameError) do @verifier.verify(valid_message) end assert_includes ["uninitialized constant MessageVerifierTest::AutoloadClass", "undefined class/module MessageVerifierTest::AutoloadClass"], exception.message end - def assert_not_verified(message) - assert_raise(ActiveSupport::MessageVerifier::InvalidSignature) do - @verifier.verify(message) + def test_raise_error_when_secret_is_nil + exception = assert_raise(ArgumentError) do + ActiveSupport::MessageVerifier.new(nil) end + assert_equal exception.message, 'Secret should not be nil.' end end - -end diff --git a/activesupport/test/multibyte_chars_test.rb b/activesupport/test/multibyte_chars_test.rb index 659fceb852..94748dd991 100644 --- a/activesupport/test/multibyte_chars_test.rb +++ b/activesupport/test/multibyte_chars_test.rb @@ -3,19 +3,12 @@ require 'abstract_unit' require 'multibyte_test_helpers' require 'active_support/core_ext/string/multibyte' -class String - def __method_for_multibyte_testing_with_integer_result; 1; end - def __method_for_multibyte_testing; 'result'; end - def __method_for_multibyte_testing!; 'result'; end - def __method_for_multibyte_testing_that_returns_nil!; end -end - class MultibyteCharsTest < ActiveSupport::TestCase include MultibyteTestHelpers def setup @proxy_class = ActiveSupport::Multibyte::Chars - @chars = @proxy_class.new UNICODE_STRING + @chars = @proxy_class.new UNICODE_STRING.dup end def test_wraps_the_original_string @@ -24,6 +17,8 @@ class MultibyteCharsTest < ActiveSupport::TestCase end def test_should_allow_method_calls_to_string + @chars.wrapped_string.singleton_class.class_eval { def __method_for_multibyte_testing; 'result'; end } + assert_nothing_raised do @chars.__method_for_multibyte_testing end @@ -33,28 +28,35 @@ class MultibyteCharsTest < ActiveSupport::TestCase end def test_forwarded_method_calls_should_return_new_chars_instance + @chars.wrapped_string.singleton_class.class_eval { def __method_for_multibyte_testing; 'result'; end } + assert_kind_of @proxy_class, @chars.__method_for_multibyte_testing assert_not_equal @chars.object_id, @chars.__method_for_multibyte_testing.object_id end def test_forwarded_bang_method_calls_should_return_the_original_chars_instance_when_result_is_not_nil + @chars.wrapped_string.singleton_class.class_eval { def __method_for_multibyte_testing!; 'result'; end } + assert_kind_of @proxy_class, @chars.__method_for_multibyte_testing! assert_equal @chars.object_id, @chars.__method_for_multibyte_testing!.object_id end def test_forwarded_bang_method_calls_should_return_nil_when_result_is_nil + @chars.wrapped_string.singleton_class.class_eval { def __method_for_multibyte_testing_that_returns_nil!; end } + assert_nil @chars.__method_for_multibyte_testing_that_returns_nil! end def test_methods_are_forwarded_to_wrapped_string_for_byte_strings - original_encoding = BYTE_STRING.encoding assert_equal BYTE_STRING.length, BYTE_STRING.mb_chars.length - ensure - BYTE_STRING.force_encoding(original_encoding) end def test_forwarded_method_with_non_string_result_should_be_returned_vertabim - assert_equal ''.__method_for_multibyte_testing_with_integer_result, @chars.__method_for_multibyte_testing_with_integer_result + str = '' + str.singleton_class.class_eval { def __method_for_multibyte_testing_with_integer_result; 1; end } + @chars.wrapped_string.singleton_class.class_eval { def __method_for_multibyte_testing_with_integer_result; 1; end } + + assert_equal str.__method_for_multibyte_testing_with_integer_result, @chars.__method_for_multibyte_testing_with_integer_result end def test_should_concatenate @@ -103,7 +105,6 @@ class MultibyteCharsUTF8BehaviourTest < ActiveSupport::TestCase @chars = UNICODE_STRING.dup.mb_chars # Ruby 1.9 only supports basic whitespace @whitespace = "\n\t " - @byte_order_mark = [65279].pack('U') end def test_split_should_return_an_array_of_chars_instances @@ -181,7 +182,7 @@ class MultibyteCharsUTF8BehaviourTest < ActiveSupport::TestCase end def test_sortability - words = %w(builder armor zebra).sort_by { |s| s.mb_chars } + words = %w(builder armor zebra).sort_by(&:mb_chars) assert_equal %w(armor builder zebra), words end diff --git a/activesupport/test/multibyte_conformance.rb b/activesupport/test/multibyte_conformance_test.rb index 2baf724da4..f7bd21c8ab 100644 --- a/activesupport/test/multibyte_conformance.rb +++ b/activesupport/test/multibyte_conformance_test.rb @@ -10,7 +10,6 @@ require 'tmpdir' class Downloader def self.download(from, to) unless File.exist?(to) - $stderr.puts "Downloading #{from} to #{to}" unless File.exist?(File.dirname(to)) system "mkdir -p #{File.dirname(to)}" end @@ -20,8 +19,9 @@ class Downloader target.write l end end - end - end + end + end + true end end @@ -31,11 +31,15 @@ class MultibyteConformanceTest < ActiveSupport::TestCase UNIDATA_URL = "http://www.unicode.org/Public/#{ActiveSupport::Multibyte::Unicode::UNICODE_VERSION}/ucd" UNIDATA_FILE = '/NormalizationTest.txt' CACHE_DIR = File.join(Dir.tmpdir, 'cache') + FileUtils.mkdir_p(CACHE_DIR) + RUN_P = begin + Downloader.download(UNIDATA_URL + UNIDATA_FILE, CACHE_DIR + UNIDATA_FILE) + rescue + end def setup - FileUtils.mkdir_p(CACHE_DIR) - Downloader.download(UNIDATA_URL + UNIDATA_FILE, CACHE_DIR + UNIDATA_FILE) @proxy = ActiveSupport::Multibyte::Chars + skip "Unable to download test data" unless RUN_P end def test_normalizations_C @@ -111,7 +115,7 @@ class MultibyteConformanceTest < ActiveSupport::TestCase next if (line.empty? || line =~ /^\#/) cols, comment = line.split("#") - cols = cols.split(";").map{|e| e.strip}.reject{|e| e.empty? } + cols = cols.split(";").map(&:strip).reject(&:empty?) next unless cols.length == 5 # codepoints are in hex in the test suite, pack wants them as integers @@ -126,4 +130,4 @@ class MultibyteConformanceTest < ActiveSupport::TestCase def inspect_codepoints(str) str.to_s.unpack("U*").map{|cp| cp.to_s(16) }.join(' ') end -end
\ No newline at end of file +end diff --git a/activesupport/test/multibyte_proxy_test.rb b/activesupport/test/multibyte_proxy_test.rb new file mode 100644 index 0000000000..d8ffd7ca9c --- /dev/null +++ b/activesupport/test/multibyte_proxy_test.rb @@ -0,0 +1,34 @@ +# encoding: utf-8 + +require 'abstract_unit' + +class MultibyteProxyText < ActiveSupport::TestCase + class AsciiOnlyEncoder + attr_reader :wrapped_string + alias to_s wrapped_string + + def initialize(string) + @wrapped_string = string.gsub(/[^\u0000-\u007F]/, '?') + end + end + + def with_custom_encoder(encoder) + original_proxy_class = ActiveSupport::Multibyte.proxy_class + + begin + ActiveSupport::Multibyte.proxy_class = encoder + + yield + ensure + ActiveSupport::Multibyte.proxy_class = original_proxy_class + end + end + + test "custom multibyte encoder" do + with_custom_encoder(AsciiOnlyEncoder) do + assert_equal "s?me string 123", "søme string 123".mb_chars.to_s + end + + assert_equal "søme string 123", "søme string 123".mb_chars.to_s + end +end diff --git a/activesupport/test/multibyte_test_helpers.rb b/activesupport/test/multibyte_test_helpers.rb index fdbe2f4350..90af2dadd4 100644 --- a/activesupport/test/multibyte_test_helpers.rb +++ b/activesupport/test/multibyte_test_helpers.rb @@ -1,9 +1,9 @@ # encoding: utf-8 module MultibyteTestHelpers - UNICODE_STRING = 'こにちわ' - ASCII_STRING = 'ohayo' - BYTE_STRING = "\270\236\010\210\245".force_encoding("ASCII-8BIT") + UNICODE_STRING = 'こにちわ'.freeze + ASCII_STRING = 'ohayo'.freeze + BYTE_STRING = "\270\236\010\210\245".force_encoding("ASCII-8BIT").freeze def chars(str) ActiveSupport::Multibyte::Chars.new(str) diff --git a/activesupport/test/number_helper_i18n_test.rb b/activesupport/test/number_helper_i18n_test.rb index 65aecece71..e6925e9083 100644 --- a/activesupport/test/number_helper_i18n_test.rb +++ b/activesupport/test/number_helper_i18n_test.rb @@ -43,6 +43,10 @@ module ActiveSupport :custom_units_for_number_to_human => {:mili => "mm", :centi => "cm", :deci => "dm", :unit => "m", :ten => "dam", :hundred => "hm", :thousand => "km"} end + def teardown + I18n.backend.reload! + end + def test_number_to_i18n_currency assert_equal("&$ - 10.00", number_to_currency(10, :locale => 'ts')) assert_equal("(&$ - 10.00)", number_to_currency(-10, :locale => 'ts')) @@ -50,8 +54,6 @@ module ActiveSupport end def test_number_to_currency_with_empty_i18n_store - I18n.backend.store_translations 'empty', {} - assert_equal("$10.00", number_to_currency(10, :locale => 'empty')) assert_equal("-$10.00", number_to_currency(-10, :locale => 'empty')) end @@ -80,8 +82,6 @@ module ActiveSupport end def test_number_with_i18n_precision_and_empty_i18n_store - I18n.backend.store_translations 'empty', {} - assert_equal("123456789.123", number_to_rounded(123456789.123456789, :locale => 'empty')) assert_equal("1.000", number_to_rounded(1.0000, :locale => 'empty')) end @@ -92,8 +92,6 @@ module ActiveSupport end def test_number_with_i18n_delimiter_and_empty_i18n_store - I18n.backend.store_translations 'empty', {} - assert_equal("1,000,000.234", number_to_delimited(1000000.234, :locale => 'empty')) end @@ -107,8 +105,6 @@ module ActiveSupport end def test_number_to_i18n_percentage_and_empty_i18n_store - I18n.backend.store_translations 'empty', {} - assert_equal("1.000%", number_to_percentage(1, :locale => 'empty')) assert_equal("1.243%", number_to_percentage(1.2434, :locale => 'empty')) assert_equal("12434.000%", number_to_percentage(12434, :locale => 'empty')) @@ -121,8 +117,6 @@ module ActiveSupport end def test_number_to_i18n_human_size_with_empty_i18n_store - I18n.backend.store_translations 'empty', {} - assert_equal("2 KB", number_to_human_size(2048, :locale => 'empty')) assert_equal("42 Bytes", number_to_human_size(42, :locale => 'empty')) end @@ -142,8 +136,6 @@ module ActiveSupport end def test_number_to_human_with_empty_i18n_store - I18n.backend.store_translations 'empty', {} - assert_equal "2 Thousand", number_to_human(2000, :locale => 'empty') assert_equal "1.23 Billion", number_to_human(1234567890, :locale => 'empty') end diff --git a/activesupport/test/number_helper_test.rb b/activesupport/test/number_helper_test.rb index 6d8d835de7..50d84a9470 100644 --- a/activesupport/test/number_helper_test.rb +++ b/activesupport/test/number_helper_test.rb @@ -1,5 +1,6 @@ require 'abstract_unit' require 'active_support/number_helper' +require 'active_support/core_ext/string/output_safety' module ActiveSupport module NumberHelper @@ -79,6 +80,9 @@ module ActiveSupport assert_equal("123.4%", number_helper.number_to_percentage(123.400, :precision => 3, :strip_insignificant_zeros => true)) assert_equal("1.000,000%", number_helper.number_to_percentage(1000, :delimiter => '.', :separator => ',')) assert_equal("1000.000 %", number_helper.number_to_percentage(1000, :format => "%n %")) + assert_equal("98a%", number_helper.number_to_percentage("98a")) + assert_equal("NaN%", number_helper.number_to_percentage(Float::NAN)) + assert_equal("Inf%", number_helper.number_to_percentage(Float::INFINITY)) end end @@ -94,6 +98,7 @@ module ActiveSupport assert_equal("123,456,789.78901", number_helper.number_to_delimited(123456789.78901)) assert_equal("0.78901", number_helper.number_to_delimited(0.78901)) assert_equal("123,456.78", number_helper.number_to_delimited("123456.78")) + assert_equal("123,456.78", number_helper.number_to_delimited("123456.78".html_safe)) end end @@ -129,6 +134,8 @@ module ActiveSupport assert_equal("111.23460000000000000000", number_helper.number_to_rounded('111.2346', :precision => 20)) assert_equal("111.23460000000000000000", number_helper.number_to_rounded(BigDecimal(111.2346, Float::DIG), :precision => 20)) assert_equal("111.2346" + "0"*96, number_helper.number_to_rounded('111.2346', :precision => 100)) + assert_equal("111.2346", number_helper.number_to_rounded(Rational(1112346, 10000), :precision => 4)) + assert_equal('0.00', number_helper.number_to_rounded(Rational(0, 1), :precision => 2)) end end @@ -169,6 +176,7 @@ module ActiveSupport assert_equal "9775.0000000000000000", number_helper.number_to_rounded(BigDecimal(9775), :precision => 20, :significant => true ) assert_equal "9775.0000000000000000", number_helper.number_to_rounded("9775", :precision => 20, :significant => true ) assert_equal "9775." + "0"*96, number_helper.number_to_rounded("9775", :precision => 100, :significant => true ) + assert_equal("97.7", number_helper.number_to_rounded(Rational(9772, 100), :precision => 3, :significant => true)) end end diff --git a/activesupport/test/option_merger_test.rb b/activesupport/test/option_merger_test.rb index 9d139b61b8..4c0364e68b 100644 --- a/activesupport/test/option_merger_test.rb +++ b/activesupport/test/option_merger_test.rb @@ -79,6 +79,15 @@ class OptionMergerTest < ActiveSupport::TestCase assert_equal ActiveSupport::OptionMerger, ActiveSupport::OptionMerger.new('', '').class end + def test_option_merger_implicit_receiver + @options.with_options foo: "bar" do + merge! fizz: "buzz" + end + + expected = { hello: "world", foo: "bar", fizz: "buzz" } + assert_equal expected, @options + end + private def method_with_options(options = {}) options diff --git a/activesupport/test/ordered_hash_test.rb b/activesupport/test/ordered_hash_test.rb index 0b54026c64..460a61613e 100644 --- a/activesupport/test/ordered_hash_test.rb +++ b/activesupport/test/ordered_hash_test.rb @@ -120,7 +120,9 @@ class OrderedHashTest < ActiveSupport::TestCase end def test_select - assert_equal @keys, @ordered_hash.select { true }.map(&:first) + new_ordered_hash = @ordered_hash.select { true } + assert_equal @keys, new_ordered_hash.map(&:first) + assert_instance_of ActiveSupport::OrderedHash, new_ordered_hash end def test_delete_if @@ -143,6 +145,7 @@ class OrderedHashTest < ActiveSupport::TestCase assert_equal copy, @ordered_hash assert !new_ordered_hash.keys.include?('pink') assert @ordered_hash.keys.include?('pink') + assert_instance_of ActiveSupport::OrderedHash, new_ordered_hash end def test_clear diff --git a/activesupport/test/rescuable_test.rb b/activesupport/test/rescuable_test.rb index ec9d231125..bd43ad0797 100644 --- a/activesupport/test/rescuable_test.rb +++ b/activesupport/test/rescuable_test.rb @@ -12,6 +12,12 @@ end class CoolError < StandardError end +module WeirdError + def self.===(other) + Exception === other && other.respond_to?(:weird?) + end +end + class Stargate attr_accessor :result @@ -29,6 +35,10 @@ class Stargate @result = e.message end + rescue_from WeirdError do + @result = 'weird' + end + def dispatch(method) send(method) rescue Exception => e @@ -47,6 +57,16 @@ class Stargate raise MadRonon.new("dex") end + def weird + StandardError.new.tap do |exc| + def exc.weird? + true + end + + raise exc + end + end + def sos @result = 'killed' end @@ -91,15 +111,20 @@ class RescuableTest < ActiveSupport::TestCase assert_equal 'dex', @stargate.result end + def test_rescue_from_error_dispatchers_with_case_operator + @stargate.dispatch :weird + assert_equal 'weird', @stargate.result + end + def test_rescues_defined_later_are_added_at_end_of_the_rescue_handlers_array - expected = ["WraithAttack", "WraithAttack", "NuclearExplosion", "MadRonon"] - result = @stargate.send(:rescue_handlers).collect {|e| e.first} + expected = ["WraithAttack", "WraithAttack", "NuclearExplosion", "MadRonon", "WeirdError"] + result = @stargate.send(:rescue_handlers).collect(&:first) assert_equal expected, result end 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} + expected = ["WraithAttack", "WraithAttack", "NuclearExplosion", "MadRonon", "WeirdError", "CoolError"] + result = @cool_stargate.send(:rescue_handlers).collect(&:first) assert_equal expected, result end end diff --git a/activesupport/test/safe_buffer_test.rb b/activesupport/test/safe_buffer_test.rb index efa9d5e61f..4532152996 100644 --- a/activesupport/test/safe_buffer_test.rb +++ b/activesupport/test/safe_buffer_test.rb @@ -165,4 +165,9 @@ class SafeBufferTest < ActiveSupport::TestCase x = 'foo %{x} bar'.html_safe % { x: 'qux' } assert x.html_safe?, 'should be safe' end + + test 'Should not affect frozen objects when accessing characters' do + x = 'Hello'.html_safe + assert_equal x[/a/, 1], nil + end end diff --git a/activesupport/test/security_utils_test.rb b/activesupport/test/security_utils_test.rb new file mode 100644 index 0000000000..08d2e3baa6 --- /dev/null +++ b/activesupport/test/security_utils_test.rb @@ -0,0 +1,9 @@ +require 'abstract_unit' +require 'active_support/security_utils' + +class SecurityUtilsTest < ActiveSupport::TestCase + def test_secure_compare_should_perform_string_comparison + assert ActiveSupport::SecurityUtils.secure_compare('a', 'a') + assert !ActiveSupport::SecurityUtils.secure_compare('a', 'b') + end +end diff --git a/activesupport/test/subscriber_test.rb b/activesupport/test/subscriber_test.rb index 253411aa3d..a88d8d9eba 100644 --- a/activesupport/test/subscriber_test.rb +++ b/activesupport/test/subscriber_test.rb @@ -4,20 +4,28 @@ require 'active_support/subscriber' class TestSubscriber < ActiveSupport::Subscriber attach_to :doodle - cattr_reader :event + cattr_reader :events def self.clear - @@event = nil + @@events = [] end def open_party(event) - @@event = event + events << event end private def private_party(event) - @@event = event + events << event + end +end + +# Monkey patch subscriber to test that only one subscriber per method is added. +class TestSubscriber + remove_method :open_party + def open_party(event) + events << event end end @@ -29,12 +37,18 @@ class SubscriberTest < ActiveSupport::TestCase def test_attaches_subscribers ActiveSupport::Notifications.instrument("open_party.doodle") - assert_equal "open_party.doodle", TestSubscriber.event.name + assert_equal "open_party.doodle", TestSubscriber.events.first.name + end + + def test_attaches_only_one_subscriber + ActiveSupport::Notifications.instrument("open_party.doodle") + + assert_equal 1, TestSubscriber.events.size end def test_does_not_attach_private_methods ActiveSupport::Notifications.instrument("private_party.doodle") - assert_nil TestSubscriber.event + assert_equal [], TestSubscriber.events end end diff --git a/activesupport/test/tagged_logging_test.rb b/activesupport/test/tagged_logging_test.rb index 27f629474e..03a63e94e8 100644 --- a/activesupport/test/tagged_logging_test.rb +++ b/activesupport/test/tagged_logging_test.rb @@ -79,6 +79,19 @@ class TaggedLoggingTest < ActiveSupport::TestCase assert_equal "[OMG] Cool story bro\n[BCX] Funky time\n", @output.string end + test "keeps each tag in their own instance" do + @other_output = StringIO.new + @other_logger = ActiveSupport::TaggedLogging.new(MyLogger.new(@other_output)) + @logger.tagged("OMG") do + @other_logger.tagged("BCX") do + @logger.info "Cool story bro" + @other_logger.info "Funky time" + end + end + assert_equal "[OMG] Cool story bro\n", @output.string + assert_equal "[BCX] Funky time\n", @other_output.string + end + test "cleans up the taggings on flush" do @logger.tagged("BCX") do Thread.new do diff --git a/activesupport/test/test_test.rb b/activesupport/test/test_case_test.rb index 8a71ef4324..151b623171 100644 --- a/activesupport/test/test_test.rb +++ b/activesupport/test/test_case_test.rb @@ -1,6 +1,4 @@ require 'abstract_unit' -require 'active_support/core_ext/date' -require 'active_support/core_ext/numeric/time' class AssertDifferenceTest < ActiveSupport::TestCase def setup @@ -28,12 +26,30 @@ class AssertDifferenceTest < ActiveSupport::TestCase assert_equal 'custom', e.message end - def test_assert_no_difference + def test_assert_no_difference_pass assert_no_difference '@object.num' do # ... end end + def test_assert_no_difference_fail + error = assert_raises(Minitest::Assertion) do + assert_no_difference '@object.num' do + @object.increment + end + end + assert_equal "\"@object.num\" didn't change by 0.\nExpected: 0\n Actual: 1", error.message + end + + def test_assert_no_difference_with_message_fail + error = assert_raises(Minitest::Assertion) do + assert_no_difference '@object.num', 'Object Changed' do + @object.increment + end + end + assert_equal "Object Changed.\n\"@object.num\" didn't change by 0.\nExpected: 0\n Actual: 1", error.message + end + def test_assert_difference assert_difference '@object.num', +1 do @object.increment @@ -157,48 +173,47 @@ class TestCaseTaggedLoggingTest < ActiveSupport::TestCase end end -class TimeHelperTest < ActiveSupport::TestCase - setup do - Time.stubs now: Time.now +class TestOrderTest < ActiveSupport::TestCase + def setup + @original_test_order = ActiveSupport::TestCase.test_order end - def test_time_helper_travel - expected_time = Time.now + 1.day - travel 1.day - - assert_equal expected_time, Time.now - assert_equal expected_time.to_date, Date.today + def teardown + ActiveSupport::TestCase.test_order = @original_test_order end - def test_time_helper_travel_with_block - expected_time = Time.now + 1.day + def test_defaults_to_random + ActiveSupport::TestCase.test_order = nil - travel 1.day do - assert_equal expected_time, Time.now - assert_equal expected_time.to_date, Date.today - end + assert_equal :random, ActiveSupport::TestCase.test_order - assert_not_equal expected_time, Time.now - assert_not_equal expected_time.to_date, Date.today + assert_equal :random, ActiveSupport.test_order end - def test_time_helper_travel_to - expected_time = Time.new(2004, 11, 24, 01, 04, 44) - travel_to expected_time + def test_test_order_is_global + ActiveSupport::TestCase.test_order = :sorted + + assert_equal :sorted, ActiveSupport.test_order + assert_equal :sorted, ActiveSupport::TestCase.test_order + assert_equal :sorted, self.class.test_order + assert_equal :sorted, Class.new(ActiveSupport::TestCase).test_order + + ActiveSupport.test_order = :random - assert_equal expected_time, Time.now - assert_equal Date.new(2004, 11, 24), Date.today + assert_equal :random, ActiveSupport.test_order + assert_equal :random, ActiveSupport::TestCase.test_order + assert_equal :random, self.class.test_order + assert_equal :random, Class.new(ActiveSupport::TestCase).test_order end - def test_time_helper_travel_to_with_block - expected_time = Time.new(2004, 11, 24, 01, 04, 44) + def test_i_suck_and_my_tests_are_order_dependent! + ActiveSupport::TestCase.test_order = :random - travel_to expected_time do - assert_equal expected_time, Time.now - assert_equal Date.new(2004, 11, 24), Date.today + klass = Class.new(ActiveSupport::TestCase) do + i_suck_and_my_tests_are_order_dependent! end - assert_not_equal expected_time, Time.now - assert_not_equal Date.new(2004, 11, 24), Date.today + assert_equal :alpha, klass.test_order + assert_equal :random, ActiveSupport::TestCase.test_order end end diff --git a/activesupport/test/testing/constant_lookup_test.rb b/activesupport/test/testing/constant_lookup_test.rb index 71a9561189..0f16419c8b 100644 --- a/activesupport/test/testing/constant_lookup_test.rb +++ b/activesupport/test/testing/constant_lookup_test.rb @@ -65,4 +65,12 @@ class ConstantLookupTest < ActiveSupport::TestCase } } end + + def test_does_not_swallow_exception_on_no_name_error_within_constant + assert_raises(NameError) do + with_autoloading_fixtures do + self.class.determine_constant_from_test_name('RaisesNameError') + end + end + end end diff --git a/activesupport/test/time_travel_test.rb b/activesupport/test/time_travel_test.rb new file mode 100644 index 0000000000..065539671d --- /dev/null +++ b/activesupport/test/time_travel_test.rb @@ -0,0 +1,72 @@ +require 'abstract_unit' +require 'active_support/core_ext/date' +require 'active_support/core_ext/numeric/time' + +class TimeTravelTest < ActiveSupport::TestCase + setup do + Time.stubs now: Time.now + end + + teardown do + travel_back + end + + def test_time_helper_travel + expected_time = Time.now + 1.day + travel 1.day + + assert_equal expected_time.to_s(:db), Time.now.to_s(:db) + assert_equal expected_time.to_date, Date.today + end + + def test_time_helper_travel_with_block + expected_time = Time.now + 1.day + + travel 1.day do + assert_equal expected_time.to_s(:db), Time.now.to_s(:db) + assert_equal expected_time.to_date, Date.today + end + + assert_not_equal expected_time.to_s(:db), Time.now.to_s(:db) + assert_not_equal expected_time.to_date, Date.today + end + + def test_time_helper_travel_to + expected_time = Time.new(2004, 11, 24, 01, 04, 44) + travel_to expected_time + + assert_equal expected_time, Time.now + assert_equal Date.new(2004, 11, 24), Date.today + end + + def test_time_helper_travel_to_with_block + expected_time = Time.new(2004, 11, 24, 01, 04, 44) + + travel_to expected_time do + assert_equal expected_time, Time.now + assert_equal Date.new(2004, 11, 24), Date.today + end + + assert_not_equal expected_time, Time.now + assert_not_equal Date.new(2004, 11, 24), Date.today + end + + def test_time_helper_travel_back + expected_time = Time.new(2004, 11, 24, 01, 04, 44) + + travel_to expected_time + assert_equal expected_time, Time.now + assert_equal Date.new(2004, 11, 24), Date.today + travel_back + + assert_not_equal expected_time, Time.now + assert_not_equal Date.new(2004, 11, 24), Date.today + end + + def test_travel_to_will_reset_the_usec_to_avoid_mysql_rouding + travel_to Time.utc(2014, 10, 10, 10, 10, 50, 999999) do + assert_equal 50, Time.now.sec + assert_equal 0, Time.now.usec + end + end +end diff --git a/activesupport/test/time_zone_test.rb b/activesupport/test/time_zone_test.rb index 1107b48460..cd7e184cda 100644 --- a/activesupport/test/time_zone_test.rb +++ b/activesupport/test/time_zone_test.rb @@ -1,7 +1,10 @@ require 'abstract_unit' require 'active_support/time' +require 'time_zone_test_helpers' class TimeZoneTest < ActiveSupport::TestCase + include TimeZoneTestHelpers + def test_utc_to_local zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)'] assert_equal Time.utc(1999, 12, 31, 19), zone.utc_to_local(Time.utc(2000, 1)) # standard offset -0500 @@ -19,7 +22,7 @@ class TimeZoneTest < ActiveSupport::TestCase assert_instance_of TZInfo::TimezonePeriod, zone.period_for_local(Time.utc(2000)) end - ActiveSupport::TimeZone::MAPPING.keys.each do |name| + ActiveSupport::TimeZone::MAPPING.each_key do |name| define_method("test_map_#{name.downcase.gsub(/[^a-z]/, '_')}_to_tzinfo") do zone = ActiveSupport::TimeZone[name] assert_respond_to zone.tzinfo, :period_for_local @@ -97,6 +100,7 @@ class TimeZoneTest < ActiveSupport::TestCase assert_equal Date.new(2000, 1, 1), ActiveSupport::TimeZone['Eastern Time (US & Canada)'].today travel_to(Time.utc(2000, 1, 2, 5)) # midnight Jan 2 EST assert_equal Date.new(2000, 1, 2), ActiveSupport::TimeZone['Eastern Time (US & Canada)'].today + travel_back end def test_tomorrow @@ -108,6 +112,7 @@ class TimeZoneTest < ActiveSupport::TestCase assert_equal Date.new(2000, 1, 2), ActiveSupport::TimeZone['Eastern Time (US & Canada)'].tomorrow travel_to(Time.utc(2000, 1, 2, 5)) # midnight Jan 2 EST assert_equal Date.new(2000, 1, 3), ActiveSupport::TimeZone['Eastern Time (US & Canada)'].tomorrow + travel_back end def test_yesterday @@ -119,6 +124,33 @@ class TimeZoneTest < ActiveSupport::TestCase assert_equal Date.new(1999, 12, 31), ActiveSupport::TimeZone['Eastern Time (US & Canada)'].yesterday travel_to(Time.utc(2000, 1, 2, 5)) # midnight Jan 2 EST assert_equal Date.new(2000, 1, 1), ActiveSupport::TimeZone['Eastern Time (US & Canada)'].yesterday + travel_back + end + + def test_travel_to_a_date + with_env_tz do + Time.use_zone('Hawaii') do + date = Date.new(2014, 2, 18) + time = date.midnight + + travel_to date do + assert_equal date, Date.current + assert_equal time, Time.current + end + end + end + end + + def test_travel_to_travels_back_and_reraises_if_the_block_raises + ts = Time.current - 1.second + + travel_to ts do + raise + end + + flunk # ensure travel_to re-raises + rescue + assert_not_equal ts, Time.current end def test_local @@ -225,6 +257,15 @@ class TimeZoneTest < ActiveSupport::TestCase assert_equal Time.utc(1999,12,31,19), twz.time end + def test_parse_with_day_omitted + with_env_tz 'US/Eastern' do + zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)'] + assert_equal Time.local(2000, 2, 1), zone.parse('Feb', Time.local(2000, 1, 1)) + assert_equal Time.local(2005, 2, 1), zone.parse('Feb 2005', Time.local(2000, 1, 1)) + assert_equal Time.local(2005, 2, 2), zone.parse('2 Feb 2005', Time.local(2000, 1, 1)) + end + end + def test_parse_should_not_black_out_system_timezone_dst_jump with_env_tz('EET') do zone = ActiveSupport::TimeZone['Pacific Time (US & Canada)'] @@ -354,15 +395,10 @@ class TimeZoneTest < ActiveSupport::TestCase assert_raise(ArgumentError) { ActiveSupport::TimeZone[false] } end - def test_unknown_zone_should_have_tzinfo_but_exception_on_utc_offset - zone = ActiveSupport::TimeZone.create("bogus") - assert_instance_of TZInfo::TimezoneProxy, zone.tzinfo - assert_raise(TZInfo::InvalidTimezoneIdentifier) { zone.utc_offset } - end - - def test_unknown_zone_with_utc_offset - zone = ActiveSupport::TimeZone.create("bogus", -21_600) - assert_equal(-21_600, zone.utc_offset) + def test_unknown_zone_raises_exception + assert_raise TZInfo::InvalidTimezoneIdentifier do + ActiveSupport::TimeZone.create("bogus") + end end def test_unknown_zones_dont_store_mapping_keys @@ -378,12 +414,4 @@ class TimeZoneTest < ActiveSupport::TestCase assert ActiveSupport::TimeZone.us_zones.include?(ActiveSupport::TimeZone["Hawaii"]) assert !ActiveSupport::TimeZone.us_zones.include?(ActiveSupport::TimeZone["Kuala Lumpur"]) 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 diff --git a/activesupport/test/time_zone_test_helpers.rb b/activesupport/test/time_zone_test_helpers.rb new file mode 100644 index 0000000000..9632b89d09 --- /dev/null +++ b/activesupport/test/time_zone_test_helpers.rb @@ -0,0 +1,16 @@ +module TimeZoneTestHelpers + def with_tz_default(tz = nil) + old_tz = Time.zone + Time.zone = tz + yield + ensure + Time.zone = old_tz + end + + 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 diff --git a/activesupport/test/transliterate_test.rb b/activesupport/test/transliterate_test.rb index e0f85f4e7c..6833ae68a7 100644 --- a/activesupport/test/transliterate_test.rb +++ b/activesupport/test/transliterate_test.rb @@ -11,7 +11,7 @@ class TransliterateTest < ActiveSupport::TestCase end def test_transliterate_should_approximate_ascii - # create string with range of Unicode"s western characters with + # create string with range of Unicode's western characters with # diacritics, excluding the division and multiplication signs which for # 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*") diff --git a/activesupport/test/xml_mini/nokogiri_engine_test.rb b/activesupport/test/xml_mini/nokogiri_engine_test.rb index 2e962576b5..1314c9065a 100644 --- a/activesupport/test/xml_mini/nokogiri_engine_test.rb +++ b/activesupport/test/xml_mini/nokogiri_engine_test.rb @@ -8,15 +8,13 @@ require 'active_support/xml_mini' require 'active_support/core_ext/hash/conversions' class NokogiriEngineTest < ActiveSupport::TestCase - include ActiveSupport - def setup - @default_backend = XmlMini.backend - XmlMini.backend = 'Nokogiri' + @default_backend = ActiveSupport::XmlMini.backend + ActiveSupport::XmlMini.backend = 'Nokogiri' end def teardown - XmlMini.backend = @default_backend + ActiveSupport::XmlMini.backend = @default_backend end def test_file_from_xml @@ -56,13 +54,13 @@ class NokogiriEngineTest < ActiveSupport::TestCase end def test_setting_nokogiri_as_backend - XmlMini.backend = 'Nokogiri' - assert_equal XmlMini_Nokogiri, XmlMini.backend + ActiveSupport::XmlMini.backend = 'Nokogiri' + assert_equal ActiveSupport::XmlMini_Nokogiri, ActiveSupport::XmlMini.backend end def test_blank_returns_empty_hash - assert_equal({}, XmlMini.parse(nil)) - assert_equal({}, XmlMini.parse('')) + assert_equal({}, ActiveSupport::XmlMini.parse(nil)) + assert_equal({}, ActiveSupport::XmlMini.parse('')) end def test_array_type_makes_an_array @@ -207,9 +205,9 @@ class NokogiriEngineTest < ActiveSupport::TestCase private def assert_equal_rexml(xml) - parsed_xml = XmlMini.parse(xml) + parsed_xml = ActiveSupport::XmlMini.parse(xml) xml.rewind if xml.respond_to?(:rewind) - hash = XmlMini.with_backend('REXML') { XmlMini.parse(xml) } + hash = ActiveSupport::XmlMini.with_backend('REXML') { ActiveSupport::XmlMini.parse(xml) } assert_equal(hash, parsed_xml) end end diff --git a/activesupport/test/xml_mini/nokogirisax_engine_test.rb b/activesupport/test/xml_mini/nokogirisax_engine_test.rb index 4f078f31e0..7978a50921 100644 --- a/activesupport/test/xml_mini/nokogirisax_engine_test.rb +++ b/activesupport/test/xml_mini/nokogirisax_engine_test.rb @@ -8,15 +8,13 @@ require 'active_support/xml_mini' require 'active_support/core_ext/hash/conversions' class NokogiriSAXEngineTest < ActiveSupport::TestCase - include ActiveSupport - def setup - @default_backend = XmlMini.backend - XmlMini.backend = 'NokogiriSAX' + @default_backend = ActiveSupport::XmlMini.backend + ActiveSupport::XmlMini.backend = 'NokogiriSAX' end def teardown - XmlMini.backend = @default_backend + ActiveSupport::XmlMini.backend = @default_backend end def test_file_from_xml @@ -57,13 +55,13 @@ class NokogiriSAXEngineTest < ActiveSupport::TestCase end def test_setting_nokogirisax_as_backend - XmlMini.backend = 'NokogiriSAX' - assert_equal XmlMini_NokogiriSAX, XmlMini.backend + ActiveSupport::XmlMini.backend = 'NokogiriSAX' + assert_equal ActiveSupport::XmlMini_NokogiriSAX, ActiveSupport::XmlMini.backend end def test_blank_returns_empty_hash - assert_equal({}, XmlMini.parse(nil)) - assert_equal({}, XmlMini.parse('')) + assert_equal({}, ActiveSupport::XmlMini.parse(nil)) + assert_equal({}, ActiveSupport::XmlMini.parse('')) end def test_array_type_makes_an_array @@ -208,9 +206,9 @@ class NokogiriSAXEngineTest < ActiveSupport::TestCase private def assert_equal_rexml(xml) - parsed_xml = XmlMini.parse(xml) + parsed_xml = ActiveSupport::XmlMini.parse(xml) xml.rewind if xml.respond_to?(:rewind) - hash = XmlMini.with_backend('REXML') { XmlMini.parse(xml) } + hash = ActiveSupport::XmlMini.with_backend('REXML') { ActiveSupport::XmlMini.parse(xml) } assert_equal(hash, parsed_xml) end end diff --git a/activesupport/test/xml_mini/rexml_engine_test.rb b/activesupport/test/xml_mini/rexml_engine_test.rb index 0c1f11803c..f0067ca656 100644 --- a/activesupport/test/xml_mini/rexml_engine_test.rb +++ b/activesupport/test/xml_mini/rexml_engine_test.rb @@ -2,19 +2,17 @@ require 'abstract_unit' require 'active_support/xml_mini' class REXMLEngineTest < ActiveSupport::TestCase - include ActiveSupport - def test_default_is_rexml - assert_equal XmlMini_REXML, XmlMini.backend + assert_equal ActiveSupport::XmlMini_REXML, ActiveSupport::XmlMini.backend end def test_set_rexml_as_backend - XmlMini.backend = 'REXML' - assert_equal XmlMini_REXML, XmlMini.backend + ActiveSupport::XmlMini.backend = 'REXML' + assert_equal ActiveSupport::XmlMini_REXML, ActiveSupport::XmlMini.backend end def test_parse_from_io - XmlMini.backend = 'REXML' + ActiveSupport::XmlMini.backend = 'REXML' io = StringIO.new(<<-eoxml) <root> good @@ -29,9 +27,9 @@ class REXMLEngineTest < ActiveSupport::TestCase private def assert_equal_rexml(xml) - parsed_xml = XmlMini.parse(xml) + parsed_xml = ActiveSupport::XmlMini.parse(xml) xml.rewind if xml.respond_to?(:rewind) - hash = XmlMini.with_backend('REXML') { XmlMini.parse(xml) } + hash = ActiveSupport::XmlMini.with_backend('REXML') { ActiveSupport::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 d992028323..f49431cbbf 100644 --- a/activesupport/test/xml_mini_test.rb +++ b/activesupport/test/xml_mini_test.rb @@ -1,6 +1,9 @@ require 'abstract_unit' require 'active_support/xml_mini' require 'active_support/builder' +require 'active_support/core_ext/array' +require 'active_support/core_ext/hash' +require 'active_support/core_ext/big_decimal' module XmlMiniTest class RenameKeyTest < ActiveSupport::TestCase @@ -88,6 +91,61 @@ module XmlMiniTest assert_xml "<b>Howdy</b>" end + test "#to_tag should use the type value in the options hash" do + @xml.to_tag(:b, "blue", @options.merge(type: 'color')) + assert_xml( "<b type=\"color\">blue</b>" ) + end + + test "#to_tag accepts symbol types" do + @xml.to_tag(:b, :name, @options) + assert_xml( "<b type=\"symbol\">name</b>" ) + end + + test "#to_tag accepts boolean types" do + @xml.to_tag(:b, true, @options) + assert_xml( "<b type=\"boolean\">true</b>") + end + + test "#to_tag accepts float types" do + @xml.to_tag(:b, 3.14, @options) + assert_xml( "<b type=\"float\">3.14</b>") + end + + test "#to_tag accepts decimal types" do + @xml.to_tag(:b, ::BigDecimal.new("1.2"), @options) + assert_xml( "<b type=\"decimal\">1.2</b>") + end + + test "#to_tag accepts date types" do + @xml.to_tag(:b, Date.new(2001,2,3), @options) + assert_xml( "<b type=\"date\">2001-02-03</b>") + end + + test "#to_tag accepts datetime types" do + @xml.to_tag(:b, DateTime.new(2001,2,3,4,5,6,'+7'), @options) + assert_xml( "<b type=\"dateTime\">2001-02-03T04:05:06+07:00</b>") + end + + test "#to_tag accepts time types" do + @xml.to_tag(:b, Time.new(1993, 02, 24, 12, 0, 0, "+09:00"), @options) + assert_xml( "<b type=\"dateTime\">1993-02-24T12:00:00+09:00</b>") + end + + test "#to_tag accepts array types" do + @xml.to_tag(:b, ["first_name", "last_name"], @options) + assert_xml( "<b type=\"array\"><b>first_name</b><b>last_name</b></b>" ) + end + + test "#to_tag accepts hash types" do + @xml.to_tag(:b, { first_name: "Bob", last_name: "Marley" }, @options) + assert_xml( "<b><first-name>Bob</first-name><last-name>Marley</last-name></b>" ) + end + + test "#to_tag should not add type when skip types option is set" do + @xml.to_tag(:b, "Bob", @options.merge(skip_types: 1)) + assert_xml( "<b>Bob</b>" ) + end + test "#to_tag should dasherize the space when passed a string with spaces as a key" do @xml.to_tag("New York", 33, @options) assert_xml "<New---York type=\"integer\">33</New---York>" @@ -97,7 +155,6 @@ module XmlMiniTest @xml.to_tag(:"New York", 33, @options) assert_xml "<New---York type=\"integer\">33</New---York>" end - # TODO: test the remaining functions hidden in #to_tag. end class WithBackendTest < ActiveSupport::TestCase @@ -169,4 +226,128 @@ module XmlMiniTest end end end + + class ParsingTest < ActiveSupport::TestCase + def setup + @parsing = ActiveSupport::XmlMini::PARSING + end + + def test_symbol + parser = @parsing['symbol'] + assert_equal :symbol, parser.call('symbol') + assert_equal :symbol, parser.call(:symbol) + assert_equal :'123', parser.call(123) + assert_raises(ArgumentError) { parser.call(Date.new(2013,11,12,02,11)) } + end + + def test_date + parser = @parsing['date'] + assert_equal Date.new(2013,11,12), parser.call("2013-11-12T0211Z") + assert_raises(TypeError) { parser.call(1384190018) } + assert_raises(ArgumentError) { parser.call("not really a date") } + end + + def test_datetime + parser = @parsing['datetime'] + assert_equal Time.new(2013,11,12,02,11,00,0), parser.call("2013-11-12T02:11:00Z") + assert_equal DateTime.new(2013,11,12), parser.call("2013-11-12T0211Z") + assert_equal DateTime.new(2013,11,12,02,11), parser.call("2013-11-12T02:11Z") + assert_equal DateTime.new(2013,11,12,02,11), parser.call("2013-11-12T11:11+9") + assert_raises(ArgumentError) { parser.call("1384190018") } + end + + def test_integer + parser = @parsing['integer'] + assert_equal 123, parser.call(123) + assert_equal 123, parser.call(123.003) + assert_equal 123, parser.call("123") + assert_equal 0, parser.call("") + assert_raises(ArgumentError) { parser.call(Date.new(2013,11,12,02,11)) } + end + + def test_float + parser = @parsing['float'] + assert_equal 123, parser.call("123") + assert_equal 123.003, parser.call("123.003") + assert_equal 123.0, parser.call("123,003") + assert_equal 0.0, parser.call("") + assert_equal 123, parser.call(123) + assert_equal 123.05, parser.call(123.05) + assert_raises(ArgumentError) { parser.call(Date.new(2013,11,12,02,11)) } + end + + def test_decimal + parser = @parsing['decimal'] + assert_equal 123, parser.call("123") + assert_equal 123.003, parser.call("123.003") + assert_equal 123.0, parser.call("123,003") + assert_equal 0.0, parser.call("") + assert_equal 123, parser.call(123) + assert_raises(ArgumentError) { parser.call(123.04) } + assert_raises(ArgumentError) { parser.call(Date.new(2013,11,12,02,11)) } + end + + def test_boolean + parser = @parsing['boolean'] + [1, true, "1"].each do |value| + assert parser.call(value) + end + + [0, false, "0"].each do |value| + assert_not parser.call(value) + end + end + + def test_string + parser = @parsing['string'] + assert_equal "123", parser.call(123) + assert_equal "123", parser.call("123") + assert_equal "[]", parser.call("[]") + assert_equal "[]", parser.call([]) + assert_equal "{}", parser.call({}) + assert_raises(ArgumentError) { parser.call(Date.new(2013,11,12,02,11)) } + end + + def test_yaml + yaml = <<YAML +product: + - sku : BL394D + quantity : 4 + description : Basketball +YAML + expected = { + "product"=> [ + {"sku"=>"BL394D", "quantity"=>4, "description"=>"Basketball"} + ] + } + parser = @parsing['yaml'] + assert_equal(expected, parser.call(yaml)) + assert_equal({1 => 'test'}, parser.call({1 => 'test'})) + assert_equal({"1 => 'test'"=>nil}, parser.call("{1 => 'test'}")) + end + + def test_base64Binary_and_binary + base64 = <<BASE64 +TWFuIGlzIGRpc3Rpbmd1aXNoZWQsIG5vdCBvbmx5IGJ5IGhpcyByZWFzb24sIGJ1dCBieSB0aGlz +IHNpbmd1bGFyIHBhc3Npb24gZnJvbSBvdGhlciBhbmltYWxzLCB3aGljaCBpcyBhIGx1c3Qgb2Yg +dGhlIG1pbmQsIHRoYXQgYnkgYSBwZXJzZXZlcmFuY2Ugb2YgZGVsaWdodCBpbiB0aGUgY29udGlu +dWVkIGFuZCBpbmRlZmF0aWdhYmxlIGdlbmVyYXRpb24gb2Yga25vd2xlZGdlLCBleGNlZWRzIHRo +ZSBzaG9ydCB2ZWhlbWVuY2Ugb2YgYW55IGNhcm5hbCBwbGVhc3VyZS4= +BASE64 + expected_base64 = <<EXPECTED +Man is distinguished, not only by his reason, but by this singular passion from +other animals, which is a lust of the mind, that by a perseverance of delight +in the continued and indefatigable generation of knowledge, exceeds the short +vehemence of any carnal pleasure. +EXPECTED + + parser = @parsing['base64Binary'] + assert_equal expected_base64.gsub(/\n/," ").strip, parser.call(base64) + parser.call("NON BASE64 INPUT") + + parser = @parsing['binary'] + assert_equal expected_base64.gsub(/\n/," ").strip, parser.call(base64, 'encoding' => 'base64') + assert_equal "IGNORED INPUT", parser.call("IGNORED INPUT", {}) + end + end end |