diff options
Diffstat (limited to 'activesupport')
35 files changed, 458 insertions, 640 deletions
diff --git a/activesupport/.gitignore b/activesupport/.gitignore new file mode 100644 index 0000000000..8cde8514fd --- /dev/null +++ b/activesupport/.gitignore @@ -0,0 +1 @@ +/test/fixtures/isolation_test/ diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md index fc47c76e99..73896be453 100644 --- a/activesupport/CHANGELOG.md +++ b/activesupport/CHANGELOG.md @@ -1,2 +1,51 @@ +## Rails 6.0.0.alpha (Unreleased) ## + +* Return all mappings for a timezone identifier in `country_zones` + + Some timezones like `Europe/London` have multiple mappings in + `ActiveSupport::TimeZone::MAPPING` so return all of them instead + of the first one found by using `Hash#value`. e.g: + + # Before + ActiveSupport::TimeZone.country_zones("GB") # => ["Edinburgh"] + + # After + ActiveSupport::TimeZone.country_zones("GB") # => ["Edinburgh", "London"] + + Fixes #31668. + + *Andrew White* + +* `String#truncate_bytes` to truncate a string to a maximum bytesize without + breaking multibyte characters or grapheme clusters like ๐ฉโ๐ฉโ๐ฆโ๐ฆ. + + *Jeremy Daer* + +* `String#strip_heredoc` preserves frozenness. + + "foo".freeze.strip_heredoc.frozen? # => true + + Fixes that frozen string literals would inadvertently become unfrozen: + + # frozen_string_literal: true + + foo = <<-MSG.strip_heredoc + la la la + MSG + + foo.frozen? # => false !?? + + *Jeremy Daer* + +* Rails 6 requires Ruby 2.4.1 or newer. + + *Jeremy Daer* + +* Adds parallel testing to Rails + + Parallelize your test suite with forked processes or threads. + + *Eileen M. Uchitelle*, *Aaron Patterson* + Please check [5-2-stable](https://github.com/rails/rails/blob/5-2-stable/activesupport/CHANGELOG.md) for previous changes. diff --git a/activesupport/activesupport.gemspec b/activesupport/activesupport.gemspec index 5d4e4e2d3b..aa695c98b2 100644 --- a/activesupport/activesupport.gemspec +++ b/activesupport/activesupport.gemspec @@ -9,7 +9,7 @@ Gem::Specification.new do |s| s.summary = "A toolkit of support libraries and Ruby core extensions extracted from the Rails framework." s.description = "A toolkit of support libraries and Ruby core extensions extracted from the Rails framework. Rich support for multibyte strings, internationalization, time zones, and testing." - s.required_ruby_version = ">= 2.2.2" + s.required_ruby_version = ">= 2.4.1" s.license = "MIT" diff --git a/activesupport/bin/generate_tables b/activesupport/bin/generate_tables deleted file mode 100755 index 18199b2171..0000000000 --- a/activesupport/bin/generate_tables +++ /dev/null @@ -1,141 +0,0 @@ -#!/usr/bin/env ruby -# frozen_string_literal: true - -begin - $:.unshift(File.expand_path("../lib", __dir__)) - require "active_support" -rescue IOError -end - -require "open-uri" -require "tmpdir" -require "fileutils" - -module ActiveSupport - module Multibyte - module Unicode - class UnicodeDatabase - def load; end - end - - class DatabaseGenerator - BASE_URI = "http://www.unicode.org/Public/#{UNICODE_VERSION}/ucd/" - SOURCES = { - codepoints: BASE_URI + "UnicodeData.txt", - composition_exclusion: BASE_URI + "CompositionExclusions.txt", - grapheme_break_property: BASE_URI + "auxiliary/GraphemeBreakProperty.txt", - cp1252: "http://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP1252.TXT" - } - - def initialize - @ucd = Unicode::UnicodeDatabase.new - end - - def parse_codepoints(line) - codepoint = Codepoint.new - raise "Could not parse input." unless line =~ /^ - ([0-9A-F]+); # code - ([^;]+); # name - ([A-Z]+); # general category - ([0-9]+); # canonical combining class - ([A-Z]+); # bidi class - (<([A-Z]*)>)? # decomposition type - ((\ ?[0-9A-F]+)*); # decomposition mapping - ([0-9]*); # decimal digit - ([0-9]*); # digit - ([^;]*); # numeric - ([YN]*); # bidi mirrored - ([^;]*); # unicode 1.0 name - ([^;]*); # iso comment - ([0-9A-F]*); # simple uppercase mapping - ([0-9A-F]*); # simple lowercase mapping - ([0-9A-F]*)$/ix # simple titlecase mapping - codepoint.code = $1.hex - codepoint.combining_class = Integer($4) - codepoint.decomp_type = $7 - codepoint.decomp_mapping = ($8 == "") ? nil : $8.split.collect(&:hex) - codepoint.uppercase_mapping = ($16 == "") ? 0 : $16.hex - codepoint.lowercase_mapping = ($17 == "") ? 0 : $17.hex - @ucd.codepoints[codepoint.code] = codepoint - end - - def parse_grapheme_break_property(line) - if line =~ /^([0-9A-F.]+)\s*;\s*([\w]+)\s*#/ - type = $2.downcase.intern - @ucd.boundary[type] ||= [] - if $1.include? ".." - parts = $1.split ".." - @ucd.boundary[type] << (parts[0].hex..parts[1].hex) - else - @ucd.boundary[type] << $1.hex - end - end - end - - def parse_composition_exclusion(line) - if line =~ /^([0-9A-F]+)/i - @ucd.composition_exclusion << $1.hex - end - end - - def parse_cp1252(line) - if line =~ /^([0-9A-Fx]+)\s([0-9A-Fx]+)/i - @ucd.cp1252[$1.hex] = $2.hex - end - end - - def create_composition_map - @ucd.codepoints.each do |_, cp| - if !cp.nil? && cp.combining_class == 0 && cp.decomp_type.nil? && !cp.decomp_mapping.nil? && cp.decomp_mapping.length == 2 && @ucd.codepoints[cp.decomp_mapping[0]].combining_class == 0 && !@ucd.composition_exclusion.include?(cp.code) - @ucd.composition_map[cp.decomp_mapping[0]] ||= {} - @ucd.composition_map[cp.decomp_mapping[0]][cp.decomp_mapping[1]] = cp.code - end - end - end - - def normalize_boundary_map - @ucd.boundary.each do |k, v| - if [:lf, :cr].include? k - @ucd.boundary[k] = v[0] - end - end - end - - def parse - SOURCES.each do |type, url| - filename = File.join(Dir.tmpdir, UNICODE_VERSION, "#{url.split('/').last}") - unless File.exist?(filename) - $stderr.puts "Downloading #{url.split('/').last}" - FileUtils.mkdir_p(File.dirname(filename)) - File.open(filename, "wb") do |target| - open(url) do |source| - source.each_line { |line| target.write line } - end - end - end - File.open(filename) do |file| - file.each_line { |line| send "parse_#{type}".intern, line } - end - end - create_composition_map - normalize_boundary_map - end - - def dump_to(filename) - File.open(filename, "wb") do |f| - f.write Marshal.dump([@ucd.codepoints, @ucd.composition_exclusion, @ucd.composition_map, @ucd.boundary, @ucd.cp1252]) - end - end - end - end - end -end - -if __FILE__ == $0 - filename = ActiveSupport::Multibyte::Unicode::UnicodeDatabase.filename - generator = ActiveSupport::Multibyte::Unicode::DatabaseGenerator.new - generator.parse - print "Writing to: #{filename}" - generator.dump_to filename - puts " (#{File.size(filename)} bytes)" -end diff --git a/activesupport/lib/active_support/cache/redis_cache_store.rb b/activesupport/lib/active_support/cache/redis_cache_store.rb index c5f8c876ba..64bc77cf22 100644 --- a/activesupport/lib/active_support/cache/redis_cache_store.rb +++ b/activesupport/lib/active_support/cache/redis_cache_store.rb @@ -17,7 +17,6 @@ end require "digest/sha2" require "active_support/core_ext/marshal" -require "active_support/core_ext/hash/transform_values" module ActiveSupport module Cache diff --git a/activesupport/lib/active_support/core_ext/hash.rb b/activesupport/lib/active_support/core_ext/hash.rb index e19aeaa983..325581a60a 100644 --- a/activesupport/lib/active_support/core_ext/hash.rb +++ b/activesupport/lib/active_support/core_ext/hash.rb @@ -8,4 +8,3 @@ 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/transform_values.rb b/activesupport/lib/active_support/core_ext/hash/transform_values.rb index 4b19c9fc1f..fc15130c9e 100644 --- a/activesupport/lib/active_support/core_ext/hash/transform_values.rb +++ b/activesupport/lib/active_support/core_ext/hash/transform_values.rb @@ -1,32 +1,5 @@ # frozen_string_literal: true -class Hash - # Returns a new hash with the results of running +block+ once for every value. - # The keys are unchanged. - # - # { a: 1, b: 2, c: 3 }.transform_values { |x| x * 2 } # => { a: 2, b: 4, c: 6 } - # - # If you do not provide a +block+, it will return an Enumerator - # for chaining with other methods: - # - # { a: 1, b: 2 }.transform_values.with_index { |v, i| [v, i].join.to_i } # => { a: 10, b: 21 } - def transform_values - return enum_for(:transform_values) { size } unless block_given? - return {} if empty? - result = self.class.new - each do |key, value| - result[key] = yield(value) - end - result - end unless method_defined? :transform_values +require "active_support/deprecation" - # Destructively converts all values using the +block+ operations. - # Same as +transform_values+ but modifies +self+. - def transform_values! - return enum_for(:transform_values!) { size } unless block_given? - each do |key, value| - self[key] = yield(value) - end - end unless method_defined? :transform_values! - # TODO: Remove this file when supporting only Ruby 2.4+. -end +ActiveSupport::Deprecation.warn "Ruby 2.4+ (required by Rails 6) provides Hash#transform_values natively, so requiring active_support/core_ext/hash/transform_values is no longer necessary. Requiring it will raise LoadError in Rails 6.1." diff --git a/activesupport/lib/active_support/core_ext/module/redefine_method.rb b/activesupport/lib/active_support/core_ext/module/redefine_method.rb index a0a6622ca4..5bd8e6e973 100644 --- a/activesupport/lib/active_support/core_ext/module/redefine_method.rb +++ b/activesupport/lib/active_support/core_ext/module/redefine_method.rb @@ -1,23 +1,14 @@ # frozen_string_literal: true class Module - if RUBY_VERSION >= "2.3" - # Marks the named method as intended to be redefined, if it exists. - # Suppresses the Ruby method redefinition warning. Prefer - # #redefine_method where possible. - def silence_redefinition_of_method(method) - if method_defined?(method) || private_method_defined?(method) - # This suppresses the "method redefined" warning; the self-alias - # looks odd, but means we don't need to generate a unique name - alias_method method, method - end - end - else - def silence_redefinition_of_method(method) - if method_defined?(method) || private_method_defined?(method) - alias_method :__rails_redefine, method - remove_method :__rails_redefine - end + # Marks the named method as intended to be redefined, if it exists. + # Suppresses the Ruby method redefinition warning. Prefer + # #redefine_method where possible. + def silence_redefinition_of_method(method) + if method_defined?(method) || private_method_defined?(method) + # This suppresses the "method redefined" warning; the self-alias + # looks odd, but means we don't need to generate a unique name + alias_method method, method end end diff --git a/activesupport/lib/active_support/core_ext/numeric/conversions.rb b/activesupport/lib/active_support/core_ext/numeric/conversions.rb index f6c2713986..7fcd0d0311 100644 --- a/activesupport/lib/active_support/core_ext/numeric/conversions.rb +++ b/activesupport/lib/active_support/core_ext/numeric/conversions.rb @@ -129,12 +129,6 @@ module ActiveSupport::NumericWithFormat end end -# Ruby 2.4+ unifies Fixnum & Bignum into Integer. -if 0.class == Integer - Integer.prepend ActiveSupport::NumericWithFormat -else - Fixnum.prepend ActiveSupport::NumericWithFormat - Bignum.prepend ActiveSupport::NumericWithFormat -end +Integer.prepend ActiveSupport::NumericWithFormat Float.prepend ActiveSupport::NumericWithFormat BigDecimal.prepend ActiveSupport::NumericWithFormat diff --git a/activesupport/lib/active_support/core_ext/object/duplicable.rb b/activesupport/lib/active_support/core_ext/object/duplicable.rb index 9bb99087bc..c78ee6bbfc 100644 --- a/activesupport/lib/active_support/core_ext/object/duplicable.rb +++ b/activesupport/lib/active_support/core_ext/object/duplicable.rb @@ -75,8 +75,11 @@ end class Symbol begin - :symbol.dup # Ruby 2.4.x. - "symbol_from_string".to_sym.dup # Some symbols can't `dup` in Ruby 2.4.0. + :symbol.dup + + # Some symbols couldn't be duped in Ruby 2.4.0 only, due to a bug. + # This feature check catches any regression. + "symbol_from_string".to_sym.dup rescue TypeError # Symbols are not duplicable: diff --git a/activesupport/lib/active_support/core_ext/regexp.rb b/activesupport/lib/active_support/core_ext/regexp.rb index efbd708aee..d92943c7ae 100644 --- a/activesupport/lib/active_support/core_ext/regexp.rb +++ b/activesupport/lib/active_support/core_ext/regexp.rb @@ -4,8 +4,4 @@ class Regexp #:nodoc: def multiline? options & MULTILINE == MULTILINE end - - def match?(string, pos = 0) - !!match(string, pos) - end unless //.respond_to?(:match?) end diff --git a/activesupport/lib/active_support/core_ext/string/filters.rb b/activesupport/lib/active_support/core_ext/string/filters.rb index 66e721eea3..df0e79afa8 100644 --- a/activesupport/lib/active_support/core_ext/string/filters.rb +++ b/activesupport/lib/active_support/core_ext/string/filters.rb @@ -78,6 +78,47 @@ class String "#{self[0, stop]}#{omission}" end + # Truncates +text+ to at most <tt>bytesize</tt> bytes in length without + # breaking string encoding by splitting multibyte characters or breaking + # grapheme clusters ("perceptual characters") by truncating at combining + # characters. + # + # >> "๐ช๐ช๐ช๐ช๐ช๐ช๐ช๐ช๐ช๐ช๐ช๐ช๐ช๐ช๐ช๐ช๐ช๐ช๐ช๐ช".size + # => 20 + # >> "๐ช๐ช๐ช๐ช๐ช๐ช๐ช๐ช๐ช๐ช๐ช๐ช๐ช๐ช๐ช๐ช๐ช๐ช๐ช๐ช".bytesize + # => 80 + # >> "๐ช๐ช๐ช๐ช๐ช๐ช๐ช๐ช๐ช๐ช๐ช๐ช๐ช๐ช๐ช๐ช๐ช๐ช๐ช๐ช".truncate_bytes(20) + # => "๐ช๐ช๐ช๐ชโฆ" + # + # The truncated text ends with the <tt>:omission</tt> string, defaulting + # to "โฆ", for a total length not exceeding <tt>bytesize</tt>. + def truncate_bytes(truncate_at, omission: "โฆ") + omission ||= "" + + case + when bytesize <= truncate_at + dup + when omission.bytesize > truncate_at + raise ArgumentError, "Omission #{omission.inspect} is #{omission.bytesize}, larger than the truncation length of #{truncate_at} bytes" + when omission.bytesize == truncate_at + omission.dup + else + self.class.new.tap do |cut| + cut_at = truncate_at - omission.bytesize + + scan(/\X/) do |grapheme| + if cut.bytesize + grapheme.bytesize <= cut_at + cut << grapheme + else + break + end + end + + cut << omission + end + end + end + # Truncates a given +text+ after a given number of words (<tt>words_count</tt>): # # 'Once upon a time in a world far far away'.truncate_words(4) diff --git a/activesupport/lib/active_support/core_ext/string/multibyte.rb b/activesupport/lib/active_support/core_ext/string/multibyte.rb index 07c0d16398..6cceb46507 100644 --- a/activesupport/lib/active_support/core_ext/string/multibyte.rb +++ b/activesupport/lib/active_support/core_ext/string/multibyte.rb @@ -11,12 +11,13 @@ class String # encapsulates the original string. A Unicode safe version of all the String methods are defined on this proxy # class. If the proxy class doesn't respond to a certain method, it's forwarded to the encapsulated string. # - # >> "ว".upcase - # => "ว" # >> "ว".mb_chars.upcase.to_s # => "ว" # - # NOTE: An above example is useful for pre Ruby 2.4. Ruby 2.4 supports Unicode case mappings. + # NOTE: Ruby 2.4 and later support native Unicode case mappings: + # + # >> "ว".upcase + # => "ว" # # == Method chaining # diff --git a/activesupport/lib/active_support/core_ext/string/strip.rb b/activesupport/lib/active_support/core_ext/string/strip.rb index cc26274e4a..6f9834bb16 100644 --- a/activesupport/lib/active_support/core_ext/string/strip.rb +++ b/activesupport/lib/active_support/core_ext/string/strip.rb @@ -20,6 +20,8 @@ class String # Technically, it looks for the least indented non-empty line # in the whole string, and removes that amount of leading whitespace. def strip_heredoc - gsub(/^#{scan(/^[ \t]*(?=\S)/).min}/, "".freeze) + gsub(/^#{scan(/^[ \t]*(?=\S)/).min}/, "".freeze).tap do |stripped| + stripped.freeze if frozen? + end end end diff --git a/activesupport/lib/active_support/duration/iso8601_serializer.rb b/activesupport/lib/active_support/duration/iso8601_serializer.rb index bb177ae5b7..84ae29c1ec 100644 --- a/activesupport/lib/active_support/duration/iso8601_serializer.rb +++ b/activesupport/lib/active_support/duration/iso8601_serializer.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true require "active_support/core_ext/object/blank" -require "active_support/core_ext/hash/transform_values" module ActiveSupport class Duration diff --git a/activesupport/lib/active_support/hash_with_indifferent_access.rb b/activesupport/lib/active_support/hash_with_indifferent_access.rb index 2e2ed8a25d..e4afc8af93 100644 --- a/activesupport/lib/active_support/hash_with_indifferent_access.rb +++ b/activesupport/lib/active_support/hash_with_indifferent_access.rb @@ -177,20 +177,18 @@ module ActiveSupport super(convert_key(key), *extras) end - if Hash.new.respond_to?(:dig) - # Same as <tt>Hash#dig</tt> where the key passed as argument can be - # either a string or a symbol: - # - # counters = ActiveSupport::HashWithIndifferentAccess.new - # counters[:foo] = { bar: 1 } - # - # counters.dig('foo', 'bar') # => 1 - # counters.dig(:foo, :bar) # => 1 - # counters.dig(:zoo) # => nil - def dig(*args) - args[0] = convert_key(args[0]) if args.size > 0 - super(*args) - end + # Same as <tt>Hash#dig</tt> where the key passed as argument can be + # either a string or a symbol: + # + # counters = ActiveSupport::HashWithIndifferentAccess.new + # counters[:foo] = { bar: 1 } + # + # counters.dig('foo', 'bar') # => 1 + # counters.dig(:foo, :bar) # => 1 + # counters.dig(:zoo) # => nil + def dig(*args) + args[0] = convert_key(args[0]) if args.size > 0 + super(*args) end # Same as <tt>Hash#default</tt> where the key passed as argument can be @@ -228,7 +226,7 @@ module ActiveSupport # hash.fetch_values('a', 'c') # => KeyError: key not found: "c" def fetch_values(*indices, &block) indices.collect { |key| fetch(key, &block) } - end if Hash.method_defined?(:fetch_values) + end # Returns a shallow copy of the hash. # @@ -311,6 +309,14 @@ module ActiveSupport dup.tap { |hash| hash.transform_keys!(*args, &block) } end + def transform_keys! + return enum_for(:transform_keys!) { size } unless block_given? + keys.each do |key| + self[yield(key)] = delete(key) + end + self + end + def slice(*keys) keys.map! { |key| convert_key(key) } self.class.new(super) diff --git a/activesupport/lib/active_support/multibyte/unicode.rb b/activesupport/lib/active_support/multibyte/unicode.rb index f923061fae..4f0e1165ef 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 = "9.0.0" + UNICODE_VERSION = RbConfig::CONFIG["UNICODE_VERSION"] # The default normalization used for operations that require # normalization. It can be set to any of the normalizations @@ -21,96 +21,13 @@ module ActiveSupport attr_accessor :default_normalization_form @default_normalization_form = :kc - # Hangul character boundaries and properties - HANGUL_SBASE = 0xAC00 - HANGUL_LBASE = 0x1100 - HANGUL_VBASE = 0x1161 - HANGUL_TBASE = 0x11A7 - HANGUL_LCOUNT = 19 - HANGUL_VCOUNT = 21 - HANGUL_TCOUNT = 28 - HANGUL_NCOUNT = HANGUL_VCOUNT * HANGUL_TCOUNT - HANGUL_SCOUNT = 11172 - HANGUL_SLAST = HANGUL_SBASE + HANGUL_SCOUNT - - # Detect whether the codepoint is in a certain character class. Returns - # +true+ when it's in the specified character class and +false+ otherwise. - # Valid character classes are: <tt>:cr</tt>, <tt>:lf</tt>, <tt>:l</tt>, - # <tt>:v</tt>, <tt>:lv</tt>, <tt>:lvt</tt> and <tt>:t</tt>. - # - # Primarily used by the grapheme cluster support. - def in_char_class?(codepoint, classes) - classes.detect { |c| database.boundary[c] === codepoint } ? true : false - end - # Unpack the string at grapheme boundaries. Returns a list of character # lists. # # Unicode.unpack_graphemes('เคเฅเคทเคฟ') # => [[2325, 2381], [2359], [2367]] # Unicode.unpack_graphemes('Cafรฉ') # => [[67], [97], [102], [233]] def unpack_graphemes(string) - codepoints = string.codepoints.to_a - unpacked = [] - pos = 0 - marker = 0 - eoc = codepoints.length - while (pos < eoc) - pos += 1 - previous = codepoints[pos - 1] - current = codepoints[pos] - - # See http://unicode.org/reports/tr29/#Grapheme_Cluster_Boundary_Rules - should_break = - if pos == eoc - true - # GB3. CR X LF - elsif previous == database.boundary[:cr] && current == database.boundary[:lf] - false - # GB4. (Control|CR|LF) รท - elsif previous && in_char_class?(previous, [:control, :cr, :lf]) - true - # GB5. รท (Control|CR|LF) - elsif in_char_class?(current, [:control, :cr, :lf]) - true - # GB6. L X (L|V|LV|LVT) - elsif database.boundary[:l] === previous && in_char_class?(current, [:l, :v, :lv, :lvt]) - false - # GB7. (LV|V) X (V|T) - elsif in_char_class?(previous, [:lv, :v]) && in_char_class?(current, [:v, :t]) - false - # GB8. (LVT|T) X (T) - elsif in_char_class?(previous, [:lvt, :t]) && database.boundary[:t] === current - false - # GB9. X (Extend | ZWJ) - elsif in_char_class?(current, [:extend, :zwj]) - false - # GB9a. X SpacingMark - elsif database.boundary[:spacingmark] === current - false - # GB9b. Prepend X - elsif database.boundary[:prepend] === previous - false - # GB10. (E_Base | EBG) Extend* X E_Modifier - elsif (marker...pos).any? { |i| in_char_class?(codepoints[i], [:e_base, :e_base_gaz]) && codepoints[i + 1...pos].all? { |c| database.boundary[:extend] === c } } && database.boundary[:e_modifier] === current - false - # GB11. ZWJ X (Glue_After_Zwj | EBG) - elsif database.boundary[:zwj] === previous && in_char_class?(current, [:glue_after_zwj, :e_base_gaz]) - false - # GB12. ^ (RI RI)* RI X RI - # GB13. [^RI] (RI RI)* RI X RI - elsif codepoints[marker..pos].all? { |c| database.boundary[:regional_indicator] === c } && codepoints[marker..pos].count { |c| database.boundary[:regional_indicator] === c }.even? - false - # GB999. Any รท Any - else - true - end - - if should_break - unpacked << codepoints[marker..pos - 1] - marker = pos - end - end - unpacked + string.scan(/\X/).map(&:codepoints) end # Reverse operation of unpack_graphemes. @@ -120,100 +37,18 @@ module ActiveSupport unpacked.flatten.pack("U*") end - # Re-order codepoints so the string becomes canonical. - def reorder_characters(codepoints) - length = codepoints.length - 1 - pos = 0 - while pos < length do - cp1, cp2 = database.codepoints[codepoints[pos]], database.codepoints[codepoints[pos + 1]] - if (cp1.combining_class > cp2.combining_class) && (cp2.combining_class > 0) - codepoints[pos..pos + 1] = cp2.code, cp1.code - pos += (pos > 0 ? -1 : 1) - else - pos += 1 - end - end - codepoints - end - # Decompose composed characters to the decomposed form. def decompose(type, codepoints) - codepoints.inject([]) do |decomposed, cp| - # if it's a hangul syllable starter character - if HANGUL_SBASE <= cp && cp < HANGUL_SLAST - sindex = cp - HANGUL_SBASE - ncp = [] # new codepoints - ncp << HANGUL_LBASE + sindex / HANGUL_NCOUNT - ncp << HANGUL_VBASE + (sindex % HANGUL_NCOUNT) / HANGUL_TCOUNT - tindex = sindex % HANGUL_TCOUNT - ncp << (HANGUL_TBASE + tindex) unless tindex == 0 - decomposed.concat ncp - # if the codepoint is decomposable in with the current decomposition type - elsif (ncp = database.codepoints[cp].decomp_mapping) && (!database.codepoints[cp].decomp_type || type == :compatibility) - decomposed.concat decompose(type, ncp.dup) - else - decomposed << cp - end + if type == :compatibility + codepoints.pack("U*").unicode_normalize(:nfkd).codepoints + else + codepoints.pack("U*").unicode_normalize(:nfd).codepoints end end # Compose decomposed characters to the composed form. def compose(codepoints) - pos = 0 - eoa = codepoints.length - 1 - starter_pos = 0 - starter_char = codepoints[0] - previous_combining_class = -1 - while pos < eoa - pos += 1 - lindex = starter_char - HANGUL_LBASE - # -- Hangul - if 0 <= lindex && lindex < HANGUL_LCOUNT - vindex = codepoints[starter_pos + 1] - HANGUL_VBASE rescue vindex = -1 - if 0 <= vindex && vindex < HANGUL_VCOUNT - tindex = codepoints[starter_pos + 2] - HANGUL_TBASE rescue tindex = -1 - if 0 <= tindex && tindex < HANGUL_TCOUNT - j = starter_pos + 2 - eoa -= 2 - else - tindex = 0 - j = starter_pos + 1 - eoa -= 1 - end - codepoints[starter_pos..j] = (lindex * HANGUL_VCOUNT + vindex) * HANGUL_TCOUNT + tindex + HANGUL_SBASE - end - starter_pos += 1 - starter_char = codepoints[starter_pos] - # -- Other characters - else - current_char = codepoints[pos] - current = database.codepoints[current_char] - if current.combining_class > previous_combining_class - if ref = database.composition_map[starter_char] - composition = ref[current_char] - else - composition = nil - end - unless composition.nil? - codepoints[starter_pos] = composition - starter_char = composition - codepoints.delete_at pos - eoa -= 1 - pos -= 1 - previous_combining_class = -1 - else - previous_combining_class = current.combining_class - end - else - previous_combining_class = current.combining_class - end - if current.combining_class == 0 - starter_pos = pos - starter_char = codepoints[pos] - end - end - end - codepoints + codepoints.pack("U*").unicode_normalize(:nfc).codepoints end # Rubinius' String#scrub, however, doesn't support ASCII-incompatible chars. @@ -266,129 +101,37 @@ module ActiveSupport def normalize(string, form = nil) form ||= @default_normalization_form # See http://www.unicode.org/reports/tr15, Table 1 - codepoints = string.codepoints.to_a case form when :d - reorder_characters(decompose(:canonical, codepoints)) + string.unicode_normalize(:nfd) when :c - compose(reorder_characters(decompose(:canonical, codepoints))) + string.unicode_normalize(:nfc) when :kd - reorder_characters(decompose(:compatibility, codepoints)) + string.unicode_normalize(:nfkd) when :kc - compose(reorder_characters(decompose(:compatibility, codepoints))) + string.unicode_normalize(:nfkc) else raise ArgumentError, "#{form} is not a valid normalization variant", caller - end.pack("U*".freeze) + end end def downcase(string) - apply_mapping string, :lowercase_mapping + string.downcase end def upcase(string) - apply_mapping string, :uppercase_mapping + string.upcase end def swapcase(string) - apply_mapping string, :swapcase_mapping - end - - # Holds data about a codepoint in the Unicode database. - class Codepoint - attr_accessor :code, :combining_class, :decomp_type, :decomp_mapping, :uppercase_mapping, :lowercase_mapping - - # Initializing Codepoint object with default values - def initialize - @combining_class = 0 - @uppercase_mapping = 0 - @lowercase_mapping = 0 - end - - def swapcase_mapping - uppercase_mapping > 0 ? uppercase_mapping : lowercase_mapping - end - end - - # Holds static data from the Unicode database. - class UnicodeDatabase - ATTRIBUTES = :codepoints, :composition_exclusion, :composition_map, :boundary, :cp1252 - - attr_writer(*ATTRIBUTES) - - def initialize - @codepoints = Hash.new(Codepoint.new) - @composition_exclusion = [] - @composition_map = {} - @boundary = {} - @cp1252 = {} - end - - # Lazy load the Unicode database so it's only loaded when it's actually used - ATTRIBUTES.each do |attr_name| - class_eval(<<-EOS, __FILE__, __LINE__ + 1) - def #{attr_name} # def codepoints - load # load - @#{attr_name} # @codepoints - end # end - EOS - end - - # Loads the Unicode database and returns all the internal objects of - # UnicodeDatabase. - def load - begin - @codepoints, @composition_exclusion, @composition_map, @boundary, @cp1252 = File.open(self.class.filename, "rb") { |f| Marshal.load f.read } - rescue => e - raise IOError.new("Couldn't load the Unicode tables for UTF8Handler (#{e.message}), ActiveSupport::Multibyte is unusable") - end - - # Redefine the === method so we can write shorter rules for grapheme cluster breaks - @boundary.each_key do |k| - @boundary[k].instance_eval do - def ===(other) - detect { |i| i === other } ? true : false - end - end if @boundary[k].kind_of?(Array) - end - - # define attr_reader methods for the instance variables - class << self - attr_reader(*ATTRIBUTES) - end - end - - # Returns the directory in which the data files are stored. - def self.dirname - File.expand_path("../values", __dir__) - end - - # Returns the filename for the data file for this version. - def self.filename - File.expand_path File.join(dirname, "unicode_tables.dat") - end + string.swapcase end private - def apply_mapping(string, mapping) - database.codepoints - string.each_codepoint.map do |codepoint| - cp = database.codepoints[codepoint] - if cp && (ncp = cp.send(mapping)) && ncp > 0 - ncp - else - codepoint - end - end.pack("U*") - end - def recode_windows1252_chars(string) string.encode(Encoding::UTF_8, Encoding::Windows_1252, invalid: :replace, undef: :replace) end - - def database - @database ||= UnicodeDatabase.new - end end end end diff --git a/activesupport/lib/active_support/rails.rb b/activesupport/lib/active_support/rails.rb index 5c34a0abb3..8b727a69ec 100644 --- a/activesupport/lib/active_support/rails.rb +++ b/activesupport/lib/active_support/rails.rb @@ -27,9 +27,3 @@ require "active_support/core_ext/module/delegation" # Defines ActiveSupport::Deprecation. require "active_support/deprecation" - -# Defines Regexp#match?. -# -# This should be removed when Rails needs Ruby 2.4 or later, and the require -# added where other Regexp extensions are being used (easy to grep). -require "active_support/core_ext/regexp" diff --git a/activesupport/lib/active_support/subscriber.rb b/activesupport/lib/active_support/subscriber.rb index d6dd5474d0..8ad39f7a05 100644 --- a/activesupport/lib/active_support/subscriber.rb +++ b/activesupport/lib/active_support/subscriber.rb @@ -54,25 +54,20 @@ module ActiveSupport @@subscribers ||= [] end - # TODO Change this to private once we've dropped Ruby 2.2 support. - # Workaround for Ruby 2.2 "private attribute?" warning. - protected - - attr_reader :subscriber, :notifier, :namespace - private + attr_reader :subscriber, :notifier, :namespace - def add_event_subscriber(event) # :doc: - return if %w{ start finish }.include?(event.to_s) + def add_event_subscriber(event) # :doc: + return if %w{ start finish }.include?(event.to_s) - pattern = "#{event}.#{namespace}" + pattern = "#{event}.#{namespace}" - # Don't add multiple subscribers (eg. if methods are redefined). - return if subscriber.patterns.include?(pattern) + # Don't add multiple subscribers (eg. if methods are redefined). + return if subscriber.patterns.include?(pattern) - subscriber.patterns << pattern - notifier.subscribe(pattern, subscriber) - end + subscriber.patterns << pattern + notifier.subscribe(pattern, subscriber) + end end attr_reader :patterns # :nodoc: diff --git a/activesupport/lib/active_support/test_case.rb b/activesupport/lib/active_support/test_case.rb index d1f7e6ea09..a698b4e61e 100644 --- a/activesupport/lib/active_support/test_case.rb +++ b/activesupport/lib/active_support/test_case.rb @@ -11,6 +11,7 @@ require "active_support/testing/isolation" require "active_support/testing/constant_lookup" require "active_support/testing/time_helpers" require "active_support/testing/file_fixtures" +require "active_support/testing/parallelization" module ActiveSupport class TestCase < ::Minitest::Test @@ -39,6 +40,91 @@ module ActiveSupport def test_order ActiveSupport.test_order ||= :random end + + # Parallelizes the test suite. + # + # Takes a `workers` argument that controls how many times the process + # is forked. For each process a new database will be created suffixed + # with the worker number. + # + # test-database-0 + # test-database-1 + # + # If `ENV["PARALLEL_WORKERS"]` is set the workers argument will be ignored + # and the environment variable will be used instead. This is useful for CI + # environments, or other environments where you may need more workers than + # you do for local testing. + # + # If the number of workers is set to `1` or fewer, the tests will not be + # parallelized. + # + # The default parallelization method is to fork processes. If you'd like to + # use threads instead you can pass `with: :threads` to the `parallelize` + # method. Note the threaded parallelization does not create multiple + # database and will not work with system tests at this time. + # + # parallelize(workers: 2, with: :threads) + # + # The threaded parallelization uses Minitest's parallel executor directly. + # The processes parallelization uses a Ruby Drb server. + def parallelize(workers: 2, with: :processes) + workers = ENV["PARALLEL_WORKERS"].to_i if ENV["PARALLEL_WORKERS"] + + return if workers <= 1 + + executor = case with + when :processes + Testing::Parallelization.new(workers) + when :threads + Minitest::Parallel::Executor.new(workers) + else + raise ArgumentError, "#{with} is not a supported parallelization executor." + end + + self.lock_threads = false if defined?(self.lock_threads) && with == :threads + + Minitest.parallel_executor = executor + + parallelize_me! + end + + # Set up hook for parallel testing. This can be used if you have multiple + # databases or any behavior that needs to be run after the process is forked + # but before the tests run. + # + # Note: this feature is not available with the threaded parallelization. + # + # In your +test_helper.rb+ add the following: + # + # class ActiveSupport::TestCase + # parallelize_setup do + # # create databases + # end + # end + def parallelize_setup(&block) + ActiveSupport::Testing::Parallelization.after_fork_hook do |worker| + yield worker + end + end + + # Clean up hook for parallel testing. This can be used to drop databases + # if your app uses multiple write/read databases or other clean up before + # the tests finish. This runs before the forked process is closed. + # + # Note: this feature is not available with the threaded parallelization. + # + # In your +test_helper.rb+ add the following: + # + # class ActiveSupport::TestCase + # parallelize_teardown do + # # drop databases + # end + # end + def parallelize_teardown(&block) + ActiveSupport::Testing::Parallelization.run_cleanup_hook do |worker| + yield worker + end + end end alias_method :method_name, :name diff --git a/activesupport/lib/active_support/testing/parallelization.rb b/activesupport/lib/active_support/testing/parallelization.rb new file mode 100644 index 0000000000..59c8486f41 --- /dev/null +++ b/activesupport/lib/active_support/testing/parallelization.rb @@ -0,0 +1,102 @@ +# frozen_string_literal: true + +require "drb" +require "drb/unix" + +module ActiveSupport + module Testing + class Parallelization # :nodoc: + class Server + include DRb::DRbUndumped + + def initialize + @queue = Queue.new + end + + def record(reporter, result) + reporter.synchronize do + reporter.record(result) + end + end + + def <<(o) + @queue << o + end + + def pop; @queue.pop; end + end + + @after_fork_hooks = [] + + def self.after_fork_hook(&blk) + @after_fork_hooks << blk + end + + def self.after_fork_hooks + @after_fork_hooks + end + + @run_cleanup_hooks = [] + + def self.run_cleanup_hook(&blk) + @run_cleanup_hooks << blk + end + + def self.run_cleanup_hooks + @run_cleanup_hooks + end + + def initialize(queue_size) + @queue_size = queue_size + @queue = Server.new + @pool = [] + + @url = DRb.start_service("drbunix:", @queue).uri + end + + def after_fork(worker) + self.class.after_fork_hooks.each do |cb| + cb.call(worker) + end + end + + def run_cleanup(worker) + self.class.run_cleanup_hooks.each do |cb| + cb.call(worker) + end + end + + def start + @pool = @queue_size.times.map do |worker| + fork do + DRb.stop_service + + after_fork(worker) + + queue = DRbObject.new_with_uri(@url) + + while job = queue.pop + klass = job[0] + method = job[1] + reporter = job[2] + result = Minitest.run_one_method(klass, method) + + queue.record(reporter, result) + end + + run_cleanup(worker) + end + end + end + + def <<(work) + @queue << work + end + + def shutdown + @queue_size.times { @queue << nil } + @pool.each { |pid| Process.waitpid pid } + end + end + end +end diff --git a/activesupport/lib/active_support/testing/time_helpers.rb b/activesupport/lib/active_support/testing/time_helpers.rb index 998a51a34c..801ea2909b 100644 --- a/activesupport/lib/active_support/testing/time_helpers.rb +++ b/activesupport/lib/active_support/testing/time_helpers.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true require "active_support/core_ext/module/redefine_method" -require "active_support/core_ext/string/strip" # for strip_heredoc require "active_support/core_ext/time/calculations" require "concurrent/map" @@ -112,7 +111,7 @@ module ActiveSupport # Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00 def travel_to(date_or_time) if block_given? && simple_stubs.stubbing(Time, :now) - travel_to_nested_block_call = <<-MSG.strip_heredoc + travel_to_nested_block_call = <<~MSG Calling `travel_to` with a block, when we have previously already made a call to `travel_to`, can lead to confusing time stubbing. diff --git a/activesupport/lib/active_support/values/time_zone.rb b/activesupport/lib/active_support/values/time_zone.rb index b75f5733b5..9dfaddb825 100644 --- a/activesupport/lib/active_support/values/time_zone.rb +++ b/activesupport/lib/active_support/values/time_zone.rb @@ -2,7 +2,6 @@ require "tzinfo" require "concurrent/map" -require "active_support/core_ext/object/blank" module ActiveSupport # The TimeZone class serves as a wrapper around TZInfo::Timezone instances. @@ -268,11 +267,14 @@ module ActiveSupport country = TZInfo::Country.get(code) country.zone_identifiers.map do |tz_id| if MAPPING.value?(tz_id) - self[MAPPING.key(tz_id)] + MAPPING.inject([]) do |memo, (key, value)| + memo << self[key] if value == tz_id + memo + end else create(tz_id, nil, TZInfo::Timezone.new(tz_id)) end - end.sort! + end.flatten(1).sort! end def zones_map diff --git a/activesupport/lib/active_support/values/unicode_tables.dat b/activesupport/lib/active_support/values/unicode_tables.dat Binary files differdeleted file mode 100644 index f7d9c48bbe..0000000000 --- a/activesupport/lib/active_support/values/unicode_tables.dat +++ /dev/null diff --git a/activesupport/lib/active_support/xml_mini.rb b/activesupport/lib/active_support/xml_mini.rb index ee2048cb54..d0659aeaae 100644 --- a/activesupport/lib/active_support/xml_mini.rb +++ b/activesupport/lib/active_support/xml_mini.rb @@ -48,10 +48,6 @@ module ActiveSupport "Array" => "array", "Hash" => "hash" } - - # No need to map these on Ruby 2.4+ - TYPE_NAMES["Fixnum"] = "integer" unless 0.class == Integer - TYPE_NAMES["Bignum"] = "integer" unless 0.class == Integer end FORMATTING = { diff --git a/activesupport/test/callbacks_test.rb b/activesupport/test/callbacks_test.rb index 30f1632460..6c7643ed72 100644 --- a/activesupport/test/callbacks_test.rb +++ b/activesupport/test/callbacks_test.rb @@ -482,10 +482,9 @@ module CallbacksTest "block in run_callbacks", "tweedle_dum", "block in run_callbacks", - ("call" if RUBY_VERSION < "2.3"), "run_callbacks", "save" - ].compact, call_stack.map(&:label) + ], call_stack.map(&:label) end def test_short_call_stack diff --git a/activesupport/test/core_ext/array/grouping_test.rb b/activesupport/test/core_ext/array/grouping_test.rb index c182b91826..37111a5d7d 100644 --- a/activesupport/test/core_ext/array/grouping_test.rb +++ b/activesupport/test/core_ext/array/grouping_test.rb @@ -4,15 +4,6 @@ require "abstract_unit" require "active_support/core_ext/array" class GroupingTest < ActiveSupport::TestCase - def setup - # In Ruby < 2.4, test we avoid Integer#/ (redefined by mathn) - Fixnum.send :private, :/ unless 0.class == Integer - end - - def teardown - Fixnum.send :public, :/ unless 0.class == Integer - end - def test_in_groups_of_with_perfect_fit groups = [] ("a".."i").to_a.in_groups_of(3) do |group| diff --git a/activesupport/test/core_ext/duration_test.rb b/activesupport/test/core_ext/duration_test.rb index c5022cf5b1..8f6befe809 100644 --- a/activesupport/test/core_ext/duration_test.rb +++ b/activesupport/test/core_ext/duration_test.rb @@ -24,7 +24,7 @@ class DurationTest < ActiveSupport::TestCase end def test_instance_of - assert 1.minute.instance_of?(1.class) + assert 1.minute.instance_of?(Integer) assert 2.days.instance_of?(ActiveSupport::Duration) assert !3.second.instance_of?(Numeric) end diff --git a/activesupport/test/core_ext/hash/transform_values_test.rb b/activesupport/test/core_ext/hash/transform_values_test.rb index d34b7fa7b9..e481b5e4a9 100644 --- a/activesupport/test/core_ext/hash/transform_values_test.rb +++ b/activesupport/test/core_ext/hash/transform_values_test.rb @@ -2,25 +2,16 @@ require "abstract_unit" require "active_support/core_ext/hash/indifferent_access" -require "active_support/core_ext/hash/transform_values" -class TransformValuesTest < ActiveSupport::TestCase - test "transform_values returns a new hash with the values computed from the block" do - original = { a: "a", b: "b" } - mapped = original.transform_values { |v| v + "!" } - - assert_equal({ a: "a", b: "b" }, original) - assert_equal({ a: "a!", b: "b!" }, mapped) - end - - test "transform_values! modifies the values of the original" do - original = { a: "a", b: "b" } - mapped = original.transform_values! { |v| v + "!" } - - assert_equal({ a: "a!", b: "b!" }, original) - assert_same original, mapped +class TransformValuesDeprecatedRequireTest < ActiveSupport::TestCase + test "requiring transform_values is deprecated" do + assert_deprecated do + require "active_support/core_ext/hash/transform_values" + end end +end +class IndifferentTransformValuesTest < ActiveSupport::TestCase test "indifferent access is still indifferent after mapping values" do original = { a: "a", b: "b" }.with_indifferent_access mapped = original.transform_values { |v| v + "!" } @@ -28,50 +19,4 @@ class TransformValuesTest < ActiveSupport::TestCase assert_equal "a!", mapped[:a] assert_equal "a!", mapped["a"] end - - # This is to be consistent with the behavior of Ruby's built in methods - # (e.g. #select, #reject) as of 2.2 - test "default values do not persist during mapping" do - original = Hash.new("foo") - original[:a] = "a" - mapped = original.transform_values { |v| v + "!" } - - assert_equal "a!", mapped[:a] - assert_nil mapped[:b] - end - - test "default procs do not persist after mapping" do - original = Hash.new { "foo" } - original[:a] = "a" - mapped = original.transform_values { |v| v + "!" } - - assert_equal "a!", mapped[:a] - assert_nil mapped[:b] - end - - test "transform_values returns a sized Enumerator if no block is given" do - original = { a: "a", b: "b" } - enumerator = original.transform_values - assert_equal original.size, enumerator.size - assert_equal Enumerator, enumerator.class - end - - test "transform_values! returns a sized Enumerator if no block is given" do - original = { a: "a", b: "b" } - enumerator = original.transform_values! - assert_equal original.size, enumerator.size - assert_equal Enumerator, enumerator.class - end - - test "transform_values is chainable with Enumerable methods" do - original = { a: "a", b: "b" } - mapped = original.transform_values.with_index { |v, i| [v, i].join } - assert_equal({ a: "a0", b: "b1" }, mapped) - end - - test "transform_values! is chainable with Enumerable methods" do - original = { a: "a", b: "b" } - original.transform_values!.with_index { |v, i| [v, i].join } - assert_equal({ a: "a0", b: "b1" }, original) - end end diff --git a/activesupport/test/core_ext/object/duplicable_test.rb b/activesupport/test/core_ext/object/duplicable_test.rb index b984becce3..635dd7f281 100644 --- a/activesupport/test/core_ext/object/duplicable_test.rb +++ b/activesupport/test/core_ext/object/duplicable_test.rb @@ -9,15 +9,9 @@ class DuplicableTest < ActiveSupport::TestCase if RUBY_VERSION >= "2.5.0" RAISE_DUP = [method(:puts)] ALLOW_DUP = ["1", "symbol_from_string".to_sym, Object.new, /foo/, [], {}, Time.now, Class.new, Module.new, BigDecimal("4.56"), nil, false, true, 1, 2.3, Complex(1), Rational(1)] - elsif RUBY_VERSION >= "2.4.1" + else RAISE_DUP = [method(:puts), Complex(1), Rational(1)] ALLOW_DUP = ["1", "symbol_from_string".to_sym, Object.new, /foo/, [], {}, Time.now, Class.new, Module.new, BigDecimal("4.56"), nil, false, true, 1, 2.3] - elsif RUBY_VERSION >= "2.4.0" # Due to 2.4.0 bug. This elsif cannot be removed unless we drop 2.4.0 support... - RAISE_DUP = [method(:puts), Complex(1), Rational(1), "symbol_from_string".to_sym] - ALLOW_DUP = ["1", Object.new, /foo/, [], {}, Time.now, Class.new, Module.new, BigDecimal("4.56"), nil, false, true, 1, 2.3] - else - RAISE_DUP = [nil, false, true, :symbol, 1, 2.3, method(:puts), Complex(1), Rational(1)] - ALLOW_DUP = ["1", Object.new, /foo/, [], {}, Time.now, Class.new, Module.new, BigDecimal("4.56")] end def test_duplicable diff --git a/activesupport/test/core_ext/string_ext_test.rb b/activesupport/test/core_ext/string_ext_test.rb index 472190277e..ccaa5dc786 100644 --- a/activesupport/test/core_ext/string_ext_test.rb +++ b/activesupport/test/core_ext/string_ext_test.rb @@ -9,9 +9,9 @@ require "constantize_test_cases" require "active_support/inflector" require "active_support/core_ext/string" require "active_support/time" -require "active_support/core_ext/string/strip" require "active_support/core_ext/string/output_safety" require "active_support/core_ext/string/indent" +require "active_support/core_ext/string/strip" require "time_zone_test_helpers" require "yaml" @@ -24,6 +24,10 @@ class StringInflectionsTest < ActiveSupport::TestCase assert_equal "", "".strip_heredoc end + def test_strip_heredoc_on_a_frozen_string + assert "".freeze.strip_heredoc.frozen? + end + def test_strip_heredoc_on_a_string_with_no_lines assert_equal "x", "x".strip_heredoc assert_equal "x", " x".strip_heredoc @@ -281,6 +285,68 @@ class StringInflectionsTest < ActiveSupport::TestCase assert_equal "Hello Big[...]", "Hello Big World!".truncate(15, omission: "[...]", separator: /\s/) end + def test_truncate_bytes + assert_equal "๐๐๐๐", "๐๐๐๐".truncate_bytes(16) + assert_equal "๐๐๐๐", "๐๐๐๐".truncate_bytes(16, omission: nil) + assert_equal "๐๐๐๐", "๐๐๐๐".truncate_bytes(16, omission: " ") + assert_equal "๐๐๐๐", "๐๐๐๐".truncate_bytes(16, omission: "๐") + + assert_equal "๐๐๐โฆ", "๐๐๐๐".truncate_bytes(15) + assert_equal "๐๐๐", "๐๐๐๐".truncate_bytes(15, omission: nil) + assert_equal "๐๐๐ ", "๐๐๐๐".truncate_bytes(15, omission: " ") + assert_equal "๐๐๐", "๐๐๐๐".truncate_bytes(15, omission: "๐") + + assert_equal "โฆ", "๐๐๐๐".truncate_bytes(5) + assert_equal "๐", "๐๐๐๐".truncate_bytes(5, omission: nil) + assert_equal "๐ ", "๐๐๐๐".truncate_bytes(5, omission: " ") + assert_equal "๐", "๐๐๐๐".truncate_bytes(5, omission: "๐") + + assert_equal "โฆ", "๐๐๐๐".truncate_bytes(4) + assert_equal "๐", "๐๐๐๐".truncate_bytes(4, omission: nil) + assert_equal " ", "๐๐๐๐".truncate_bytes(4, omission: " ") + assert_equal "๐", "๐๐๐๐".truncate_bytes(4, omission: "๐") + + assert_raise ArgumentError do + "๐๐๐๐".truncate_bytes(3, omission: "๐") + end + end + + def test_truncate_bytes_preserves_codepoints + assert_equal "๐๐๐๐", "๐๐๐๐".truncate_bytes(16) + assert_equal "๐๐๐๐", "๐๐๐๐".truncate_bytes(16, omission: nil) + assert_equal "๐๐๐๐", "๐๐๐๐".truncate_bytes(16, omission: " ") + assert_equal "๐๐๐๐", "๐๐๐๐".truncate_bytes(16, omission: "๐") + + assert_equal "๐๐๐โฆ", "๐๐๐๐".truncate_bytes(15) + assert_equal "๐๐๐", "๐๐๐๐".truncate_bytes(15, omission: nil) + assert_equal "๐๐๐ ", "๐๐๐๐".truncate_bytes(15, omission: " ") + assert_equal "๐๐๐", "๐๐๐๐".truncate_bytes(15, omission: "๐") + + assert_equal "โฆ", "๐๐๐๐".truncate_bytes(5) + assert_equal "๐", "๐๐๐๐".truncate_bytes(5, omission: nil) + assert_equal "๐ ", "๐๐๐๐".truncate_bytes(5, omission: " ") + assert_equal "๐", "๐๐๐๐".truncate_bytes(5, omission: "๐") + + assert_equal "โฆ", "๐๐๐๐".truncate_bytes(4) + assert_equal "๐", "๐๐๐๐".truncate_bytes(4, omission: nil) + assert_equal " ", "๐๐๐๐".truncate_bytes(4, omission: " ") + assert_equal "๐", "๐๐๐๐".truncate_bytes(4, omission: "๐") + + assert_raise ArgumentError do + "๐๐๐๐".truncate_bytes(3, omission: "๐") + end + end + + def test_truncates_bytes_preserves_grapheme_clusters + assert_equal "a ", "a โค๏ธ b".truncate_bytes(2, omission: nil) + assert_equal "a ", "a โค๏ธ b".truncate_bytes(3, omission: nil) + assert_equal "a ", "a โค๏ธ b".truncate_bytes(7, omission: nil) + assert_equal "a โค๏ธ", "a โค๏ธ b".truncate_bytes(8, omission: nil) + + assert_equal "a ", "a ๐ฉโโค๏ธโ๐ฉ".truncate_bytes(13, omission: nil) + assert_equal "", "๐ฉโโค๏ธโ๐ฉ".truncate_bytes(13, omission: nil) + end + def test_truncate_words assert_equal "Hello Big World!", "Hello Big World!".truncate_words(3) assert_equal "Hello Big...", "Hello Big World!".truncate_words(2) diff --git a/activesupport/test/core_ext/time_with_zone_test.rb b/activesupport/test/core_ext/time_with_zone_test.rb index be3b8a49a9..2ea5f0921c 100644 --- a/activesupport/test/core_ext/time_with_zone_test.rb +++ b/activesupport/test/core_ext/time_with_zone_test.rb @@ -3,7 +3,6 @@ require "abstract_unit" require "active_support/time" require "time_zone_test_helpers" -require "active_support/core_ext/string/strip" require "yaml" class TimeWithZoneTest < ActiveSupport::TestCase @@ -163,7 +162,7 @@ class TimeWithZoneTest < ActiveSupport::TestCase end def test_to_yaml - yaml = <<-EOF.strip_heredoc + yaml = <<~EOF --- !ruby/object:ActiveSupport::TimeWithZone utc: 2000-01-01 00:00:00.000000000 Z zone: !ruby/object:ActiveSupport::TimeZone @@ -175,7 +174,7 @@ class TimeWithZoneTest < ActiveSupport::TestCase end def test_ruby_to_yaml - yaml = <<-EOF.strip_heredoc + yaml = <<~EOF --- twz: !ruby/object:ActiveSupport::TimeWithZone utc: 2000-01-01 00:00:00.000000000 Z @@ -188,7 +187,7 @@ class TimeWithZoneTest < ActiveSupport::TestCase end def test_yaml_load - yaml = <<-EOF.strip_heredoc + yaml = <<~EOF --- !ruby/object:ActiveSupport::TimeWithZone utc: 2000-01-01 00:00:00.000000000 Z zone: !ruby/object:ActiveSupport::TimeZone @@ -200,7 +199,7 @@ class TimeWithZoneTest < ActiveSupport::TestCase end def test_ruby_yaml_load - yaml = <<-EOF.strip_heredoc + yaml = <<~EOF --- twz: !ruby/object:ActiveSupport::TimeWithZone utc: 2000-01-01 00:00:00.000000000 Z diff --git a/activesupport/test/hash_with_indifferent_access_test.rb b/activesupport/test/hash_with_indifferent_access_test.rb index 41d653fa59..b06250baf8 100644 --- a/activesupport/test/hash_with_indifferent_access_test.rb +++ b/activesupport/test/hash_with_indifferent_access_test.rb @@ -169,8 +169,6 @@ class HashWithIndifferentAccessTest < ActiveSupport::TestCase end def test_indifferent_fetch_values - skip unless Hash.method_defined?(:fetch_values) - @mixed = @mixed.with_indifferent_access assert_equal [1, 2], @mixed.fetch_values("a", "b") @@ -404,6 +402,12 @@ class HashWithIndifferentAccessTest < ActiveSupport::TestCase assert_equal({ "aa" => 1, "bb" => 2 }, hash) assert_instance_of ActiveSupport::HashWithIndifferentAccess, hash + + hash = ActiveSupport::HashWithIndifferentAccess.new(@strings).transform_keys { |k| k.to_sym } + + assert_equal(1, hash[:a]) + assert_equal(1, hash["a"]) + assert_instance_of ActiveSupport::HashWithIndifferentAccess, hash end def test_indifferent_transform_keys_bang @@ -412,6 +416,13 @@ class HashWithIndifferentAccessTest < ActiveSupport::TestCase assert_equal({ "aa" => 1, "bb" => 2 }, indifferent_strings) assert_instance_of ActiveSupport::HashWithIndifferentAccess, indifferent_strings + + indifferent_strings = ActiveSupport::HashWithIndifferentAccess.new(@strings) + indifferent_strings.transform_keys! { |k| k.to_sym } + + assert_equal(1, indifferent_strings[:a]) + assert_equal(1, indifferent_strings["a"]) + assert_instance_of ActiveSupport::HashWithIndifferentAccess, indifferent_strings end def test_indifferent_transform_values @@ -562,7 +573,6 @@ class HashWithIndifferentAccessTest < ActiveSupport::TestCase end def test_nested_dig_indifferent_access - skip if RUBY_VERSION < "2.3.0" data = { "this" => { "views" => 1234 } }.with_indifferent_access assert_equal 1234, data.dig(:this, :views) end diff --git a/activesupport/test/multibyte_unicode_database_test.rb b/activesupport/test/multibyte_unicode_database_test.rb deleted file mode 100644 index 540a34493d..0000000000 --- a/activesupport/test/multibyte_unicode_database_test.rb +++ /dev/null @@ -1,26 +0,0 @@ -# frozen_string_literal: true - -require "abstract_unit" - -class MultibyteUnicodeDatabaseTest < ActiveSupport::TestCase - include ActiveSupport::Multibyte::Unicode - - def setup - @ucd = UnicodeDatabase.new - end - - UnicodeDatabase::ATTRIBUTES.each do |attribute| - define_method "test_lazy_loading_on_attribute_access_of_#{attribute}" do - assert_called(@ucd, :load) do - @ucd.send(attribute) - end - end - end - - def test_load - @ucd.load - UnicodeDatabase::ATTRIBUTES.each do |attribute| - assert @ucd.send(attribute).length > 1 - end - end -end diff --git a/activesupport/test/time_zone_test.rb b/activesupport/test/time_zone_test.rb index 405c8f315b..63ca22efb5 100644 --- a/activesupport/test/time_zone_test.rb +++ b/activesupport/test/time_zone_test.rb @@ -756,6 +756,16 @@ class TimeZoneTest < ActiveSupport::TestCase assert_not_includes ActiveSupport::TimeZone.country_zones(:ru), ActiveSupport::TimeZone["Kuala Lumpur"] end + def test_country_zones_with_and_without_mappings + assert_includes ActiveSupport::TimeZone.country_zones("au"), ActiveSupport::TimeZone["Adelaide"] + assert_includes ActiveSupport::TimeZone.country_zones("au"), ActiveSupport::TimeZone["Australia/Lord_Howe"] + end + + def test_country_zones_with_multiple_mappings + assert_includes ActiveSupport::TimeZone.country_zones("gb"), ActiveSupport::TimeZone["Edinburgh"] + assert_includes ActiveSupport::TimeZone.country_zones("gb"), ActiveSupport::TimeZone["London"] + end + def test_country_zones_without_mappings assert_includes ActiveSupport::TimeZone.country_zones(:sv), ActiveSupport::TimeZone["America/El_Salvador"] end |