From 8bdc9e8d8793c4ce3eb25f56f0d229166261248e Mon Sep 17 00:00:00 2001 From: Michael Hoy Date: Mon, 26 Sep 2016 16:51:52 -0500 Subject: number_to_rounded_converter: extract rounding logic --- activesupport/lib/active_support/number_helper.rb | 1 + .../number_helper/number_to_rounded_converter.rb | 32 ++--------- .../number_helper/rounding_helper.rb | 64 ++++++++++++++++++++++ 3 files changed, 71 insertions(+), 26 deletions(-) create mode 100644 activesupport/lib/active_support/number_helper/rounding_helper.rb (limited to 'activesupport') diff --git a/activesupport/lib/active_support/number_helper.rb b/activesupport/lib/active_support/number_helper.rb index 880340ca86..9cb2821cb6 100644 --- a/activesupport/lib/active_support/number_helper.rb +++ b/activesupport/lib/active_support/number_helper.rb @@ -4,6 +4,7 @@ module ActiveSupport eager_autoload do autoload :NumberConverter + autoload :RoundingHelper autoload :NumberToRoundedConverter autoload :NumberToDelimitedConverter autoload :NumberToHumanConverter 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 1f013990ea..c32d85a45f 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 @@ -5,26 +5,14 @@ module ActiveSupport self.validate_float = true def convert - precision = options.delete :precision + helper = RoundingHelper.new(options) + rounded_number = helper.round(number) - if precision - case number - when Float, String - @number = BigDecimal(number.to_s) - when Rational - @number = BigDecimal(number, digit_count(number.to_i) + precision) - else - @number = number.to_d - end - - if options.delete(:significant) && precision > 0 - digits, rounded_number = digits_and_rounded_number(precision) + if precision = options[:precision] + if options[:significant] && precision > 0 + digits = helper.digit_count(rounded_number) precision -= digits precision = 0 if precision < 0 # don't let it be negative - else - rounded_number = number.round(precision) - rounded_number = rounded_number.to_i if precision == 0 && rounded_number.finite? - rounded_number = rounded_number.abs if rounded_number.zero? # prevent showing negative zeros end formatted_string = @@ -38,7 +26,7 @@ module ActiveSupport "%00.#{precision}f" % rounded_number end else - formatted_string = number + formatted_string = rounded_number end delimited_number = NumberToDelimitedConverter.convert(formatted_string, options) @@ -79,14 +67,6 @@ module ActiveSupport number end end - - def absolute_number(number) - number.respond_to?(:abs) ? number.abs : number.to_d.abs - end - - def zero? - number.respond_to?(:zero?) ? number.zero? : number.to_d.zero? - end end end end diff --git a/activesupport/lib/active_support/number_helper/rounding_helper.rb b/activesupport/lib/active_support/number_helper/rounding_helper.rb new file mode 100644 index 0000000000..d9644df17d --- /dev/null +++ b/activesupport/lib/active_support/number_helper/rounding_helper.rb @@ -0,0 +1,64 @@ +module ActiveSupport + module NumberHelper + class RoundingHelper # :nodoc: + attr_reader :options + + def initialize(options) + @options = options + end + + def round(number) + return number unless precision + number = convert_to_decimal(number) + if significant && precision > 0 + round_significant(number) + else + round_without_significant(number) + end + end + + def digit_count(number) + return 1 if number.zero? + (Math.log10(absolute_number(number)) + 1).floor + end + + private + def round_without_significant(number) + number = number.round(precision) + number = number.to_i if precision == 0 && number.finite? + number = number.abs if number.zero? # prevent showing negative zeros + number + end + + def round_significant(number) + return 0 if number.zero? + digits = digit_count(number) + multiplier = 10**(digits - precision) + (number / BigDecimal.new(multiplier.to_f.to_s)).round * multiplier + end + + def convert_to_decimal(number) + case number + when Float, String + number = BigDecimal(number.to_s) + when Rational + number = BigDecimal(number, digit_count(number.to_i) + precision) + else + number = number.to_d + end + end + + def precision + options[:precision] + end + + def significant + options[:significant] + end + + def absolute_number(number) + number.respond_to?(:abs) ? number.abs : number.to_d.abs + end + end + end +end -- cgit v1.2.3 From 202aadd4f4bc0bdce7e376add98c36098ff5086b Mon Sep 17 00:00:00 2001 From: Michael Hoy Date: Mon, 26 Sep 2016 17:09:37 -0500 Subject: number_to_human_converter: round before calculating exponent fixes #25664 --- .../lib/active_support/number_helper/number_to_human_converter.rb | 6 ++---- activesupport/test/number_helper_test.rb | 6 ++++++ 2 files changed, 8 insertions(+), 4 deletions(-) (limited to 'activesupport') 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 56185ddf4b..040343b5dd 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 @@ -9,6 +9,7 @@ module ActiveSupport self.validate_float = true def convert # :nodoc: + @number = RoundingHelper.new(options).round(number) @number = Float(number) # for backwards compatibility with those that didn't add strip_insignificant_zeros to their locale files @@ -20,10 +21,7 @@ module ActiveSupport exponent = calculate_exponent(units) @number = number / (10**exponent) - until (rounded_number = NumberToRoundedConverter.convert(number, options)) != NumberToRoundedConverter.convert(1000, options) - @number = number / 1000.0 - exponent += 3 - end + rounded_number = NumberToRoundedConverter.convert(number, options) unit = determine_unit(units, exponent) format.gsub("%n".freeze, rounded_number).gsub("%u".freeze, unit).strip end diff --git a/activesupport/test/number_helper_test.rb b/activesupport/test/number_helper_test.rb index dc0c34d4e2..4caf1428ea 100644 --- a/activesupport/test/number_helper_test.rb +++ b/activesupport/test/number_helper_test.rb @@ -321,12 +321,18 @@ module ActiveSupport gangster = { hundred: "hundred bucks", million: "thousand quids" } assert_equal "1 hundred bucks", number_helper.number_to_human(100, units: gangster) assert_equal "25 hundred bucks", number_helper.number_to_human(2500, units: gangster) + assert_equal "1000 hundred bucks", number_helper.number_to_human(100_000, units: gangster) + assert_equal "1 thousand quids", number_helper.number_to_human(999_999, units: gangster) + assert_equal "1 thousand quids", number_helper.number_to_human(1_000_000, units: gangster) assert_equal "25 thousand quids", number_helper.number_to_human(25000000, units: gangster) assert_equal "12300 thousand quids", number_helper.number_to_human(12345000000, units: gangster) #Spaces are stripped from the resulting string assert_equal "4", number_helper.number_to_human(4, units: { unit: "", ten: "tens " }) assert_equal "4.5 tens", number_helper.number_to_human(45, units: { unit: "", ten: " tens " }) + + #Uses only the provided units and does not try to use larger ones + assert_equal "1000 kilometers", number_helper.number_to_human(1_000_000, units: { unit: "meter", thousand: "kilometers" }) end end -- cgit v1.2.3 From 0584e21ac03a7abba100a8820aeab8de32facc2d Mon Sep 17 00:00:00 2001 From: Ryuta Kamizono Date: Sun, 14 May 2017 02:43:02 +0900 Subject: Remove returning true in internal callbacks `display_deprecation_warning_for_false_terminator` was removed since 3a25cdc. --- activesupport/lib/active_support/i18n_railtie.rb | 4 ---- 1 file changed, 4 deletions(-) (limited to 'activesupport') diff --git a/activesupport/lib/active_support/i18n_railtie.rb b/activesupport/lib/active_support/i18n_railtie.rb index f05c707ccd..51fe6f3418 100644 --- a/activesupport/lib/active_support/i18n_railtie.rb +++ b/activesupport/lib/active_support/i18n_railtie.rb @@ -66,10 +66,6 @@ module I18n app.reloaders << reloader app.reloader.to_run do reloader.execute_if_updated { require_unload_lock! } - # 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 -- cgit v1.2.3 From 75fa8dd309a84e125b59d01bf182d88419631eaa Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Thu, 18 May 2017 18:12:32 +0200 Subject: Use recyclable cache keys (#29092) --- activesupport/CHANGELOG.md | 6 ++ activesupport/lib/active_support/cache.rb | 78 +++++++++++++++---- .../lib/active_support/cache/mem_cache_store.rb | 8 +- activesupport/test/caching_test.rb | 90 ++++++++++++++++++++++ 4 files changed, 164 insertions(+), 18 deletions(-) (limited to 'activesupport') diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md index 3ce4a0bbab..49ff2c88d0 100644 --- a/activesupport/CHANGELOG.md +++ b/activesupport/CHANGELOG.md @@ -1,3 +1,9 @@ +* Add support for versioned cache entries. This enables the cache stores to recycle cache keys, greatly saving + on storage in cases with frequent churn. Works together with the separation of #cache_key and #cache_version + in Active Record and its use in Action Pack's fragment caching. + + *DHH* + * Pass gem name and deprecation horizon to deprecation notifications. *Willem van Bergen* diff --git a/activesupport/lib/active_support/cache.rb b/activesupport/lib/active_support/cache.rb index 4d8c2046e8..258140fe1d 100644 --- a/activesupport/lib/active_support/cache.rb +++ b/activesupport/lib/active_support/cache.rb @@ -219,6 +219,10 @@ module ActiveSupport # cache = ActiveSupport::Cache::MemoryStore.new(expires_in: 5.minutes) # cache.write(key, value, expires_in: 1.minute) # Set a lower value for one entry # + # Setting :version verifies the cache stored under name + # is of the same version. nil is returned on mismatches despite contents. + # This feature is used to support recyclable cache keys. + # # Setting :race_condition_ttl is very useful in situations where # a cache entry is used very frequently and is under heavy load. If a # cache expires and due to heavy load several different processes will try @@ -287,6 +291,7 @@ module ActiveSupport instrument(:read, name, options) do |payload| cached_entry = read_entry(key, options) unless options[:force] entry = handle_expired_entry(cached_entry, key, options) + entry = nil if entry && entry.mismatched?(normalize_version(name, options)) payload[:super_operation] = :fetch if payload payload[:hit] = !!entry if payload end @@ -303,20 +308,33 @@ module ActiveSupport end end - # Fetches data from the cache, using the given key. If there is data in + # Reads data from the cache, using the given key. If there is data in # the cache with the given key, then that data is returned. Otherwise, # +nil+ is returned. # + # Note, if data was written with the :expires_in or :version options, + # both of these conditions are applied before the data is returned. + # # Options are passed to the underlying cache implementation. def read(name, options = nil) options = merged_options(options) - key = normalize_key(name, options) + key = normalize_key(name, options) + version = normalize_version(name, options) + instrument(:read, name, options) do |payload| entry = read_entry(key, options) + if entry if entry.expired? delete_entry(key, options) payload[:hit] = false if payload + nil + elsif entry.mismatched?(version) + if payload + payload[:hit] = false + payload[:mismatch] = "#{entry.version} != #{version}" + end + nil else payload[:hit] = true if payload @@ -341,11 +359,15 @@ module ActiveSupport results = {} names.each do |name| - key = normalize_key(name, options) - entry = read_entry(key, options) + key = normalize_key(name, options) + version = normalize_version(name, options) + entry = read_entry(key, options) + if entry if entry.expired? delete_entry(key, options) + elsif entry.mismatched?(version) + # Skip mismatched versions else results[name] = entry.value end @@ -396,7 +418,7 @@ module ActiveSupport options = merged_options(options) instrument(:write, name, options) do - entry = Entry.new(value, options) + entry = Entry.new(value, options.merge(version: normalize_version(name, options))) write_entry(normalize_key(name, options), entry, options) end end @@ -420,7 +442,7 @@ module ActiveSupport instrument(:exist?, name) do entry = read_entry(normalize_key(name, options), options) - (entry && !entry.expired?) || false + (entry && !entry.expired? && !entry.mismatched?(normalize_version(name, options))) || false end end @@ -517,6 +539,17 @@ module ActiveSupport end end + + # Prefixes a key with the namespace. Namespace and key will be delimited + # with a colon. + def normalize_key(key, options) + key = expanded_key(key) + namespace = options[:namespace] if options + prefix = namespace.is_a?(Proc) ? namespace.call : namespace + key = "#{prefix}:#{key}" if prefix + key + end + # Expands key to be a consistent string value. Invokes +cache_key+ if # object responds to +cache_key+. Otherwise, +to_param+ method will be # called. If the key is a Hash, then keys will be sorted alphabetically. @@ -537,14 +570,16 @@ module ActiveSupport key.to_param end - # Prefixes a key with the namespace. Namespace and key will be delimited - # with a colon. - def normalize_key(key, options) - key = expanded_key(key) - namespace = options[:namespace] if options - prefix = namespace.is_a?(Proc) ? namespace.call : namespace - key = "#{prefix}:#{key}" if prefix - key + def normalize_version(key, options = nil) + (options && options[:version].try(:to_param)) || expanded_version(key) + end + + def expanded_version(key) + case + when key.respond_to?(:cache_version) then key.cache_version.to_param + when key.is_a?(Array) then key.map { |element| expanded_version(element) }.compact.to_param + when key.respond_to?(:to_a) then expanded_version(key.to_a) + end end def instrument(operation, key, options = nil) @@ -555,6 +590,7 @@ module ActiveSupport ActiveSupport::Notifications.instrument("cache_#{operation}.active_support", payload) { yield(payload) } end + def log return unless logger && logger.debug? && !silence? logger.debug(yield) @@ -591,13 +627,16 @@ module ActiveSupport end end - # This class is used to represent cache entries. Cache entries have a value and an optional - # expiration time. The expiration time is used to support the :race_condition_ttl option - # on the cache. + # This class is used to represent cache entries. Cache entries have a value, an optional + # expiration time, and an optional version. The expiration time is used to support the :race_condition_ttl option + # on the cache. The version is used to support the :version option on the cache for rejecting + # mismatches. # # Since cache entries in most instances will be serialized, the internals of this class are highly optimized # using short instance variable names that are lazily defined. class Entry # :nodoc: + attr_reader :version + DEFAULT_COMPRESS_LIMIT = 16.kilobytes # Creates a new cache entry for the specified value. Options supported are @@ -610,6 +649,7 @@ module ActiveSupport @value = value end + @version = options[:version] @created_at = Time.now.to_f @expires_in = options[:expires_in] @expires_in = @expires_in.to_f if @expires_in @@ -619,6 +659,10 @@ module ActiveSupport compressed? ? uncompress(@value) : @value end + def mismatched?(version) + @version && version && @version != version + end + # Checks if the entry is expired. The +expires_in+ parameter can override # the value set when the entry was created. def expired? diff --git a/activesupport/lib/active_support/cache/mem_cache_store.rb b/activesupport/lib/active_support/cache/mem_cache_store.rb index e09cee3335..06fa9f67ad 100644 --- a/activesupport/lib/active_support/cache/mem_cache_store.rb +++ b/activesupport/lib/active_support/cache/mem_cache_store.rb @@ -97,12 +97,18 @@ module ActiveSupport options = merged_options(options) keys_to_names = Hash[names.map { |name| [normalize_key(name, options), name] }] + raw_values = @data.get_multi(keys_to_names.keys) values = {} + raw_values.each do |key, value| entry = deserialize_entry(value) - values[keys_to_names[key]] = entry.value unless entry.expired? + + unless entry.expired? || entry.mismatched?(normalize_version(keys_to_names[key], options)) + values[keys_to_names[key]] = entry.value + end end + values end diff --git a/activesupport/test/caching_test.rb b/activesupport/test/caching_test.rb index dbec684ce0..f53b98c73e 100644 --- a/activesupport/test/caching_test.rb +++ b/activesupport/test/caching_test.rb @@ -579,6 +579,93 @@ module CacheStoreBehavior end end +module CacheStoreVersionBehavior + ModelWithKeyAndVersion = Struct.new(:cache_key, :cache_version) + + def test_fetch_with_right_version_should_hit + @cache.fetch("foo", version: 1) { "bar" } + assert_equal "bar", @cache.read("foo", version: 1) + end + + def test_fetch_with_wrong_version_should_miss + @cache.fetch("foo", version: 1) { "bar" } + assert_nil @cache.read("foo", version: 2) + end + + def test_read_with_right_version_should_hit + @cache.write("foo", "bar", version: 1) + assert_equal "bar", @cache.read("foo", version: 1) + end + + def test_read_with_wrong_version_should_miss + @cache.write("foo", "bar", version: 1) + assert_nil @cache.read("foo", version: 2) + end + + def test_exist_with_right_version_should_be_true + @cache.write("foo", "bar", version: 1) + assert @cache.exist?("foo", version: 1) + end + + def test_exist_with_wrong_version_should_be_false + @cache.write("foo", "bar", version: 1) + assert !@cache.exist?("foo", version: 2) + end + + def test_reading_and_writing_with_model_supporting_cache_version + m1v1 = ModelWithKeyAndVersion.new("model/1", 1) + m1v2 = ModelWithKeyAndVersion.new("model/1", 2) + + @cache.write(m1v1, "bar") + assert_equal "bar", @cache.read(m1v1) + assert_nil @cache.read(m1v2) + end + + def test_reading_and_writing_with_model_supporting_cache_version_using_nested_key + m1v1 = ModelWithKeyAndVersion.new("model/1", 1) + m1v2 = ModelWithKeyAndVersion.new("model/1", 2) + + @cache.write([ "something", m1v1 ], "bar") + assert_equal "bar", @cache.read([ "something", m1v1 ]) + assert_nil @cache.read([ "something", m1v2 ]) + end + + def test_fetching_with_model_supporting_cache_version + m1v1 = ModelWithKeyAndVersion.new("model/1", 1) + m1v2 = ModelWithKeyAndVersion.new("model/1", 2) + + @cache.fetch(m1v1) { "bar" } + assert_equal "bar", @cache.fetch(m1v1) { "bu" } + assert_equal "bu", @cache.fetch(m1v2) { "bu" } + end + + def test_exist_with_model_supporting_cache_version + m1v1 = ModelWithKeyAndVersion.new("model/1", 1) + m1v2 = ModelWithKeyAndVersion.new("model/1", 2) + + @cache.write(m1v1, "bar") + assert @cache.exist?(m1v1) + assert_not @cache.fetch(m1v2) + end + + def test_fetch_multi_with_model_supporting_cache_version + m1v1 = ModelWithKeyAndVersion.new("model/1", 1) + m2v1 = ModelWithKeyAndVersion.new("model/2", 1) + m2v2 = ModelWithKeyAndVersion.new("model/2", 2) + + first_fetch_values = @cache.fetch_multi(m1v1, m2v1) { |m| m.cache_key } + second_fetch_values = @cache.fetch_multi(m1v1, m2v2) { |m| m.cache_key + " 2nd" } + + assert_equal({ m1v1 => "model/1", m2v1 => "model/2" }, first_fetch_values) + assert_equal({ m1v1 => "model/1", m2v2 => "model/2 2nd" }, second_fetch_values) + end + + def test_version_is_normalized + @cache.write("foo", "bar", version: 1) + assert_equal "bar", @cache.read("foo", version: "1") + end +end + # https://rails.lighthouseapp.com/projects/8994/tickets/6225-memcachestore-cant-deal-with-umlauts-and-special-characters # The error is caused by character encodings that can't be compared with ASCII-8BIT regular expressions and by special # characters like the umlaut in UTF-8. @@ -822,6 +909,7 @@ class FileStoreTest < ActiveSupport::TestCase end include CacheStoreBehavior + include CacheStoreVersionBehavior include LocalCacheBehavior include CacheDeleteMatchedBehavior include CacheIncrementDecrementBehavior @@ -931,6 +1019,7 @@ class MemoryStoreTest < ActiveSupport::TestCase end include CacheStoreBehavior + include CacheStoreVersionBehavior include CacheDeleteMatchedBehavior include CacheIncrementDecrementBehavior @@ -1052,6 +1141,7 @@ class MemCacheStoreTest < ActiveSupport::TestCase end include CacheStoreBehavior + include CacheStoreVersionBehavior include LocalCacheBehavior include CacheIncrementDecrementBehavior include EncodedKeyCacheBehavior -- cgit v1.2.3 From b9b4fa9154e7c81ddb2bac4c5d53a9cb98c3351e Mon Sep 17 00:00:00 2001 From: Ryuta Kamizono Date: Fri, 19 May 2017 08:28:15 +0900 Subject: Cleanup CHANGELOGs [ci skip] * Fix indentation. * Add backticks. --- activesupport/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'activesupport') diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md index 49ff2c88d0..c50c1902fe 100644 --- a/activesupport/CHANGELOG.md +++ b/activesupport/CHANGELOG.md @@ -1,5 +1,5 @@ * Add support for versioned cache entries. This enables the cache stores to recycle cache keys, greatly saving - on storage in cases with frequent churn. Works together with the separation of #cache_key and #cache_version + on storage in cases with frequent churn. Works together with the separation of `#cache_key` and `#cache_version` in Active Record and its use in Action Pack's fragment caching. *DHH* -- cgit v1.2.3 From 7c45146b15e682de11251180eaa4e75ac50e07cd Mon Sep 17 00:00:00 2001 From: Eilis Hamilton Date: Wed, 10 May 2017 09:50:47 +1200 Subject: Fix pluralization of uncountables when given a locale Previously apply_inflections would only use the :en uncountables rather then the ones for the locale that was passed to pluralize or singularize. This changes apply_inflections to take a locale which it will use to find the uncountables. --- activesupport/CHANGELOG.md | 4 ++++ activesupport/lib/active_support/inflector/methods.rb | 15 +++++++++------ activesupport/test/inflector_test.rb | 7 +++++++ 3 files changed, 20 insertions(+), 6 deletions(-) (limited to 'activesupport') diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md index c50c1902fe..e15f54ae8f 100644 --- a/activesupport/CHANGELOG.md +++ b/activesupport/CHANGELOG.md @@ -1,3 +1,7 @@ +* Fix Inflector#apply_inflections to use a locale for uncountables + + *Eilis Hamilton* + * Add support for versioned cache entries. This enables the cache stores to recycle cache keys, greatly saving on storage in cases with frequent churn. Works together with the separation of `#cache_key` and `#cache_version` in Active Record and its use in Action Pack's fragment caching. diff --git a/activesupport/lib/active_support/inflector/methods.rb b/activesupport/lib/active_support/inflector/methods.rb index 1b089a7538..ff1a0cb8c7 100644 --- a/activesupport/lib/active_support/inflector/methods.rb +++ b/activesupport/lib/active_support/inflector/methods.rb @@ -28,7 +28,7 @@ module ActiveSupport # pluralize('CamelOctopus') # => "CamelOctopi" # pluralize('ley', :es) # => "leyes" def pluralize(word, locale = :en) - apply_inflections(word, inflections(locale).plurals) + apply_inflections(word, inflections(locale).plurals, locale) end # The reverse of #pluralize, returns the singular form of a word in a @@ -45,7 +45,7 @@ module ActiveSupport # singularize('CamelOctopi') # => "CamelOctopus" # singularize('leyes', :es) # => "ley" def singularize(word, locale = :en) - apply_inflections(word, inflections(locale).singulars) + apply_inflections(word, inflections(locale).singulars, locale) end # Converts strings to UpperCamelCase. @@ -387,12 +387,15 @@ module ActiveSupport # Applies inflection rules for +singularize+ and +pluralize+. # - # apply_inflections('post', inflections.plurals) # => "posts" - # apply_inflections('posts', inflections.singulars) # => "post" - def apply_inflections(word, rules) + # If passed an optional +locale+ parameter, the uncountables will be + # found for that locale. + # + # apply_inflections('post', inflections.plurals, :en) # => "posts" + # apply_inflections('posts', inflections.singulars, :en) # => "post" + def apply_inflections(word, rules, locale = :en) result = word.to_s.dup - if word.empty? || inflections.uncountables.uncountable?(result) + if word.empty? || inflections(locale).uncountables.uncountable?(result) result else rules.each { |(rule, replacement)| break if result.sub!(rule, replacement) } diff --git a/activesupport/test/inflector_test.rb b/activesupport/test/inflector_test.rb index 14bc10513b..ef956eda90 100644 --- a/activesupport/test/inflector_test.rb +++ b/activesupport/test/inflector_test.rb @@ -420,6 +420,8 @@ class InflectorTest < ActiveSupport::TestCase inflect.singular(/es$/, "") inflect.irregular("el", "los") + + inflect.uncountable("agua") end assert_equal("hijos", "hijo".pluralize(:es)) @@ -432,12 +434,17 @@ class InflectorTest < ActiveSupport::TestCase assert_equal("los", "el".pluralize(:es)) assert_equal("els", "el".pluralize) + assert_equal("agua", "agua".pluralize(:es)) + assert_equal("aguas", "agua".pluralize) + ActiveSupport::Inflector.inflections(:es) { |inflect| inflect.clear } assert ActiveSupport::Inflector.inflections(:es).plurals.empty? assert ActiveSupport::Inflector.inflections(:es).singulars.empty? + assert ActiveSupport::Inflector.inflections(:es).uncountables.empty? assert !ActiveSupport::Inflector.inflections.plurals.empty? assert !ActiveSupport::Inflector.inflections.singulars.empty? + assert !ActiveSupport::Inflector.inflections.uncountables.empty? end def test_clear_all -- cgit v1.2.3 From aa8749eb52d7919a438940c9218cad98d892f9ad Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Fri, 19 May 2017 14:09:09 +0200 Subject: Add cache_key_with_version and use it in ActiveSupport::Cache.expand_cache_key This retains the existing behavior of ActiveSupport::Cache.expand_cache_key (as used by etaging) where the cache key includes the version. --- activesupport/lib/active_support/cache.rb | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) (limited to 'activesupport') diff --git a/activesupport/lib/active_support/cache.rb b/activesupport/lib/active_support/cache.rb index 258140fe1d..cea603e1b3 100644 --- a/activesupport/lib/active_support/cache.rb +++ b/activesupport/lib/active_support/cache.rb @@ -88,10 +88,11 @@ module ActiveSupport private def retrieve_cache_key(key) case - when key.respond_to?(:cache_key) then key.cache_key - when key.is_a?(Array) then key.map { |element| retrieve_cache_key(element) }.to_param - when key.respond_to?(:to_a) then retrieve_cache_key(key.to_a) - else key.to_param + when key.respond_to?(:cache_key_with_version) then key.cache_key_with_version + when key.respond_to?(:cache_key) then key.cache_key + when key.is_a?(Array) then key.map { |element| retrieve_cache_key(element) }.to_param + when key.respond_to?(:to_a) then retrieve_cache_key(key.to_a) + else key.to_param end.to_s end -- cgit v1.2.3 From dccfcbfdafb2bd77a7d130cb69e0c3725d7f7c6d Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Sat, 20 May 2017 16:43:31 +0200 Subject: Remove unused mismatch payload attribute --- activesupport/lib/active_support/cache.rb | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) (limited to 'activesupport') diff --git a/activesupport/lib/active_support/cache.rb b/activesupport/lib/active_support/cache.rb index cea603e1b3..44f80dd379 100644 --- a/activesupport/lib/active_support/cache.rb +++ b/activesupport/lib/active_support/cache.rb @@ -331,11 +331,7 @@ module ActiveSupport payload[:hit] = false if payload nil elsif entry.mismatched?(version) - if payload - payload[:hit] = false - payload[:mismatch] = "#{entry.version} != #{version}" - end - + payload[:hit] = false if payload nil else payload[:hit] = true if payload -- cgit v1.2.3 From 28938dd64c8b4fa1943d0b878d3d832b94fa12a3 Mon Sep 17 00:00:00 2001 From: Andrew White Date: Sat, 20 May 2017 16:33:09 +0100 Subject: Fix implicit calculations with scalars and durations Previously calculations where the scalar is first would be converted to a duration of seconds but this causes issues with dates being converted to times, e.g: Time.zone = "Beijing" # => Asia/Shanghai date = Date.civil(2017, 5, 20) # => Mon, 20 May 2017 2 * 1.day # => 172800 seconds date + 2 * 1.day # => Mon, 22 May 2017 00:00:00 CST +08:00 Now the `ActiveSupport::Duration::Scalar` calculation methods will try to maintain the part structure of the duration where possible, e.g: Time.zone = "Beijing" # => Asia/Shanghai date = Date.civil(2017, 5, 20) # => Mon, 20 May 2017 2 * 1.day # => 2 days date + 2 * 1.day # => Mon, 22 May 2017 Fixes #29160, #28970. --- activesupport/CHANGELOG.md | 22 +++++++++++++++ activesupport/lib/active_support/duration.rb | 41 ++++++++++++++++++++++++---- activesupport/test/core_ext/duration_test.rb | 34 +++++++++++++++++++++++ 3 files changed, 91 insertions(+), 6 deletions(-) (limited to 'activesupport') diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md index c50c1902fe..bae573cf37 100644 --- a/activesupport/CHANGELOG.md +++ b/activesupport/CHANGELOG.md @@ -1,3 +1,25 @@ +* Fix implicit coercion calculations with scalars and durations + + Previously calculations where the scalar is first would be converted to a duration + of seconds but this causes issues with dates being converted to times, e.g: + + Time.zone = "Beijing" # => Asia/Shanghai + date = Date.civil(2017, 5, 20) # => Mon, 20 May 2017 + 2 * 1.day # => 172800 seconds + date + 2 * 1.day # => Mon, 22 May 2017 00:00:00 CST +08:00 + + Now the `ActiveSupport::Duration::Scalar` calculation methods will try to maintain + the part structure of the duration where possible, e.g: + + Time.zone = "Beijing" # => Asia/Shanghai + date = Date.civil(2017, 5, 20) # => Mon, 20 May 2017 + 2 * 1.day # => 2 days + date + 2 * 1.day # => Mon, 22 May 2017 + + Fixes #29160, #28970. + + *Andrew White* + * Add support for versioned cache entries. This enables the cache stores to recycle cache keys, greatly saving on storage in cases with frequent churn. Works together with the separation of `#cache_key` and `#cache_version` in Active Record and its use in Action Pack's fragment caching. diff --git a/activesupport/lib/active_support/duration.rb b/activesupport/lib/active_support/duration.rb index d4424ed792..39deb2313f 100644 --- a/activesupport/lib/active_support/duration.rb +++ b/activesupport/lib/active_support/duration.rb @@ -37,27 +37,56 @@ module ActiveSupport end def +(other) - calculate(:+, other) + if Duration === other + seconds = value + other.parts[:seconds] + new_parts = other.parts.merge(seconds: seconds) + new_value = value + other.value + + Duration.new(new_value, new_parts) + else + calculate(:+, other) + end end def -(other) - calculate(:-, other) + if Duration === other + seconds = value - other.parts[:seconds] + new_parts = other.parts.map { |part, other_value| [part, -other_value] }.to_h + new_parts = new_parts.merge(seconds: seconds) + new_value = value - other.value + + Duration.new(new_value, new_parts) + else + calculate(:-, other) + end end def *(other) - calculate(:*, other) + if Duration === other + new_parts = other.parts.map { |part, other_value| [part, value * other_value] }.to_h + new_value = value * other.value + + Duration.new(new_value, new_parts) + else + calculate(:*, other) + end end def /(other) - calculate(:/, other) + if Duration === other + new_parts = other.parts.map { |part, other_value| [part, value / other_value] }.to_h + new_value = new_parts.inject(0) { |total, (part, value)| total + value * Duration::PARTS_IN_SECONDS[part] } + + Duration.new(new_value, new_parts) + else + calculate(:/, other) + end end private def calculate(op, other) if Scalar === other Scalar.new(value.public_send(op, other.value)) - elsif Duration === other - Duration.seconds(value).public_send(op, other) elsif Numeric === other Scalar.new(value.public_send(op, other)) else diff --git a/activesupport/test/core_ext/duration_test.rb b/activesupport/test/core_ext/duration_test.rb index 1648a9b270..3108f24f21 100644 --- a/activesupport/test/core_ext/duration_test.rb +++ b/activesupport/test/core_ext/duration_test.rb @@ -337,6 +337,13 @@ class DurationTest < ActiveSupport::TestCase assert_equal "no implicit conversion of String into ActiveSupport::Duration::Scalar", exception.message end + def test_scalar_plus_parts + scalar = ActiveSupport::Duration::Scalar.new(10) + + assert_equal({ days: 1, seconds: 10 }, (scalar + 1.day).parts) + assert_equal({ days: -1, seconds: 10 }, (scalar + -1.day).parts) + end + def test_scalar_minus scalar = ActiveSupport::Duration::Scalar.new(10) @@ -349,6 +356,9 @@ class DurationTest < ActiveSupport::TestCase assert_equal 5, scalar - 5.seconds assert_instance_of ActiveSupport::Duration, scalar - 5.seconds + assert_equal({ days: -1, seconds: 10 }, (scalar - 1.day).parts) + assert_equal({ days: 1, seconds: 10 }, (scalar - -1.day).parts) + exception = assert_raises(TypeError) do scalar - "foo" end @@ -356,6 +366,13 @@ class DurationTest < ActiveSupport::TestCase assert_equal "no implicit conversion of String into ActiveSupport::Duration::Scalar", exception.message end + def test_scalar_minus_parts + scalar = ActiveSupport::Duration::Scalar.new(10) + + assert_equal({ days: -1, seconds: 10 }, (scalar - 1.day).parts) + assert_equal({ days: 1, seconds: 10 }, (scalar - -1.day).parts) + end + def test_scalar_multiply scalar = ActiveSupport::Duration::Scalar.new(5) @@ -375,6 +392,14 @@ class DurationTest < ActiveSupport::TestCase assert_equal "no implicit conversion of String into ActiveSupport::Duration::Scalar", exception.message end + def test_scalar_multiply_parts + scalar = ActiveSupport::Duration::Scalar.new(1) + assert_equal({ days: 2 }, (scalar * 2.days).parts) + assert_equal(172800, (scalar * 2.days).value) + assert_equal({ days: -2 }, (scalar * -2.days).parts) + assert_equal(-172800, (scalar * -2.days).value) + end + def test_scalar_divide scalar = ActiveSupport::Duration::Scalar.new(10) @@ -394,6 +419,15 @@ class DurationTest < ActiveSupport::TestCase assert_equal "no implicit conversion of String into ActiveSupport::Duration::Scalar", exception.message end + def test_scalar_divide_parts + scalar = ActiveSupport::Duration::Scalar.new(10) + + assert_equal({ days: 2 }, (scalar / 5.days).parts) + assert_equal(172800, (scalar / 5.days).value) + assert_equal({ days: -2 }, (scalar / -5.days).parts) + assert_equal(-172800, (scalar / -5.days).value) + end + def test_twelve_months_equals_one_year assert_equal 12.months, 1.year end -- cgit v1.2.3 From 40bdbce191ad90dfea43dad51fac5c4726b89392 Mon Sep 17 00:00:00 2001 From: bogdanvlviv Date: Mon, 15 May 2017 14:17:28 +0000 Subject: Define path with __dir__ ".. with __dir__ we can restore order in the Universe." - by @fxn Related to 5b8738c2df003a96f0e490c43559747618d10f5f --- activesupport/activesupport.gemspec | 2 +- activesupport/bin/generate_tables | 2 +- activesupport/lib/active_support/core_ext.rb | 2 +- .../lib/active_support/deprecation/reporting.rb | 2 +- activesupport/lib/active_support/i18n.rb | 2 +- .../lib/active_support/multibyte/unicode.rb | 2 +- activesupport/test/dependencies_test.rb | 20 ++++++++++---------- activesupport/test/dependencies_test_helpers.rb | 2 +- activesupport/test/testing/file_fixtures_test.rb | 4 ++-- activesupport/test/xml_mini/jdom_engine_test.rb | 2 +- 10 files changed, 20 insertions(+), 20 deletions(-) (limited to 'activesupport') diff --git a/activesupport/activesupport.gemspec b/activesupport/activesupport.gemspec index 08370cba85..ed277c81ef 100644 --- a/activesupport/activesupport.gemspec +++ b/activesupport/activesupport.gemspec @@ -1,4 +1,4 @@ -version = File.read(File.expand_path("../../RAILS_VERSION", __FILE__)).strip +version = File.read(File.expand_path("../RAILS_VERSION", __dir__)).strip Gem::Specification.new do |s| s.platform = Gem::Platform::RUBY diff --git a/activesupport/bin/generate_tables b/activesupport/bin/generate_tables index aa36a01b5b..6f62593f14 100755 --- a/activesupport/bin/generate_tables +++ b/activesupport/bin/generate_tables @@ -1,7 +1,7 @@ #!/usr/bin/env ruby begin - $:.unshift(File.expand_path(File.dirname(__FILE__) + "/../lib")) + $:.unshift(File.expand_path("../lib", __dir__)) require "active_support" rescue IOError end diff --git a/activesupport/lib/active_support/core_ext.rb b/activesupport/lib/active_support/core_ext.rb index f397f658f3..42e0acf66a 100644 --- a/activesupport/lib/active_support/core_ext.rb +++ b/activesupport/lib/active_support/core_ext.rb @@ -1,3 +1,3 @@ -(Dir["#{File.dirname(__FILE__)}/core_ext/*.rb"]).each do |path| +Dir.glob(File.expand_path("core_ext/*.rb", __dir__)).each do |path| require path end diff --git a/activesupport/lib/active_support/deprecation/reporting.rb b/activesupport/lib/active_support/deprecation/reporting.rb index 851d8eeda1..140bdccbb3 100644 --- a/activesupport/lib/active_support/deprecation/reporting.rb +++ b/activesupport/lib/active_support/deprecation/reporting.rb @@ -102,7 +102,7 @@ module ActiveSupport end end - RAILS_GEM_ROOT = File.expand_path("../../../../..", __FILE__) + "/" + RAILS_GEM_ROOT = File.expand_path("../../../..", __dir__) def ignored_callstack(path) path.start_with?(RAILS_GEM_ROOT) || path.start_with?(RbConfig::CONFIG["rubylibdir"]) diff --git a/activesupport/lib/active_support/i18n.rb b/activesupport/lib/active_support/i18n.rb index f0408f429c..1a1f1a1257 100644 --- a/activesupport/lib/active_support/i18n.rb +++ b/activesupport/lib/active_support/i18n.rb @@ -10,4 +10,4 @@ end require "active_support/lazy_load_hooks" ActiveSupport.run_load_hooks(:i18n) -I18n.load_path << "#{File.dirname(__FILE__)}/locale/en.yml" +I18n.load_path << File.expand_path("locale/en.yml", __dir__) diff --git a/activesupport/lib/active_support/multibyte/unicode.rb b/activesupport/lib/active_support/multibyte/unicode.rb index 0912912aba..8223e45e5a 100644 --- a/activesupport/lib/active_support/multibyte/unicode.rb +++ b/activesupport/lib/active_support/multibyte/unicode.rb @@ -357,7 +357,7 @@ module ActiveSupport # Returns the directory in which the data files are stored. def self.dirname - File.dirname(__FILE__) + "/../values/" + File.expand_path("../values", __dir__) end # Returns the filename for the data file for this version. diff --git a/activesupport/test/dependencies_test.rb b/activesupport/test/dependencies_test.rb index e38d4e83e5..1ea36418ff 100644 --- a/activesupport/test/dependencies_test.rb +++ b/activesupport/test/dependencies_test.rb @@ -104,7 +104,7 @@ class DependenciesTest < ActiveSupport::TestCase 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}") + expanded = File.expand_path("dependencies/#{filename}", __dir__) $check_warnings_load_count = 0 assert_not ActiveSupport::Dependencies.loaded.include?(expanded) @@ -293,7 +293,7 @@ class DependenciesTest < ActiveSupport::TestCase end def test_doesnt_break_normal_require - path = File.expand_path("../autoloading_fixtures/load_path", __FILE__) + path = File.expand_path("autoloading_fixtures/load_path", __dir__) original_path = $:.dup $:.push(path) with_autoloading_fixtures do @@ -312,7 +312,7 @@ class DependenciesTest < ActiveSupport::TestCase end def test_doesnt_break_normal_require_nested - path = File.expand_path("../autoloading_fixtures/load_path", __FILE__) + path = File.expand_path("autoloading_fixtures/load_path", __dir__) original_path = $:.dup $:.push(path) @@ -332,7 +332,7 @@ class DependenciesTest < ActiveSupport::TestCase end def test_require_returns_true_when_file_not_yet_required - path = File.expand_path("../autoloading_fixtures/load_path", __FILE__) + path = File.expand_path("autoloading_fixtures/load_path", __dir__) original_path = $:.dup $:.push(path) @@ -345,7 +345,7 @@ class DependenciesTest < ActiveSupport::TestCase 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__) + path = File.expand_path("autoloading_fixtures/load_path", __dir__) original_path = $:.dup $:.push(path) @@ -359,7 +359,7 @@ class DependenciesTest < ActiveSupport::TestCase end def test_require_returns_false_when_file_already_required - path = File.expand_path("../autoloading_fixtures/load_path", __FILE__) + path = File.expand_path("autoloading_fixtures/load_path", __dir__) original_path = $:.dup $:.push(path) @@ -379,7 +379,7 @@ class DependenciesTest < ActiveSupport::TestCase end def test_load_returns_true_when_file_found - path = File.expand_path("../autoloading_fixtures/load_path", __FILE__) + path = File.expand_path("autoloading_fixtures/load_path", __dir__) original_path = $:.dup $:.push(path) @@ -438,7 +438,7 @@ class DependenciesTest < ActiveSupport::TestCase def test_loadable_constants_for_path_should_handle_relative_paths fake_root = "dependencies" - relative_root = File.dirname(__FILE__) + "/dependencies" + relative_root = File.expand_path("dependencies", __dir__) ["", "/"].each do |suffix| with_loading fake_root + suffix do assert_equal ["A::B"], ActiveSupport::Dependencies.loadable_constants_for_path(relative_root + "/a/b") @@ -463,7 +463,7 @@ class DependenciesTest < ActiveSupport::TestCase end def test_loadable_constants_with_load_path_without_trailing_slash - path = File.dirname(__FILE__) + "/autoloading_fixtures/class_folder/inline_class.rb" + path = File.expand_path("autoloading_fixtures/class_folder/inline_class.rb", __dir__) with_loading "autoloading_fixtures/class/" do assert_equal [], ActiveSupport::Dependencies.loadable_constants_for_path(path) end @@ -991,7 +991,7 @@ class DependenciesTest < ActiveSupport::TestCase def test_remove_constant_does_not_trigger_loading_autoloads constant = "ShouldNotBeAutoloaded" Object.class_eval do - autoload constant, File.expand_path("../autoloading_fixtures/should_not_be_required", __FILE__) + autoload constant, File.expand_path("autoloading_fixtures/should_not_be_required", __dir__) end assert_nil ActiveSupport::Dependencies.remove_constant(constant), "Kernel#autoload has been triggered by remove_constant" diff --git a/activesupport/test/dependencies_test_helpers.rb b/activesupport/test/dependencies_test_helpers.rb index 9bc63ed89e..451195a143 100644 --- a/activesupport/test/dependencies_test_helpers.rb +++ b/activesupport/test/dependencies_test_helpers.rb @@ -1,7 +1,7 @@ module DependenciesTestHelpers def with_loading(*from) old_mechanism, ActiveSupport::Dependencies.mechanism = ActiveSupport::Dependencies.mechanism, :load - this_dir = File.dirname(__FILE__) + this_dir = __dir__ parent_dir = File.dirname(this_dir) path_copy = $LOAD_PATH.dup $LOAD_PATH.unshift(parent_dir) unless $LOAD_PATH.include?(parent_dir) diff --git a/activesupport/test/testing/file_fixtures_test.rb b/activesupport/test/testing/file_fixtures_test.rb index faa81b5e75..9f28252c31 100644 --- a/activesupport/test/testing/file_fixtures_test.rb +++ b/activesupport/test/testing/file_fixtures_test.rb @@ -3,7 +3,7 @@ require "abstract_unit" require "pathname" class FileFixturesTest < ActiveSupport::TestCase - self.file_fixture_path = File.expand_path("../../file_fixtures", __FILE__) + self.file_fixture_path = File.expand_path("../file_fixtures", __dir__) test "#file_fixture returns Pathname to file fixture" do path = file_fixture("sample.txt") @@ -20,7 +20,7 @@ class FileFixturesTest < ActiveSupport::TestCase end class FileFixturesPathnameDirectoryTest < ActiveSupport::TestCase - self.file_fixture_path = Pathname.new(File.expand_path("../../file_fixtures", __FILE__)) + self.file_fixture_path = Pathname.new(File.expand_path("../file_fixtures", __dir__)) test "#file_fixture_path returns Pathname to file fixture" do path = file_fixture("sample.txt") diff --git a/activesupport/test/xml_mini/jdom_engine_test.rb b/activesupport/test/xml_mini/jdom_engine_test.rb index e783cea67c..fc35ac113b 100644 --- a/activesupport/test/xml_mini/jdom_engine_test.rb +++ b/activesupport/test/xml_mini/jdom_engine_test.rb @@ -2,7 +2,7 @@ require_relative "xml_mini_engine_test" XMLMiniEngineTest.run_with_platform("java") do class JDOMEngineTest < XMLMiniEngineTest - FILES_DIR = File.dirname(__FILE__) + "/../fixtures/xml" + FILES_DIR = File.expand_path("../fixtures/xml", __dir__) def test_not_allowed_to_expand_entities_to_files attack_xml = <<-EOT -- cgit v1.2.3 From bef95d372f9104a321cf17ac9f45316e9caac920 Mon Sep 17 00:00:00 2001 From: Koichi ITO Date: Wed, 24 May 2017 15:21:02 +0900 Subject: Fix a RuboCop offences using `rubocop -a` --- activesupport/lib/active_support/cache.rb | 2 -- 1 file changed, 2 deletions(-) (limited to 'activesupport') diff --git a/activesupport/lib/active_support/cache.rb b/activesupport/lib/active_support/cache.rb index 44f80dd379..a1093a2e23 100644 --- a/activesupport/lib/active_support/cache.rb +++ b/activesupport/lib/active_support/cache.rb @@ -536,7 +536,6 @@ module ActiveSupport end end - # Prefixes a key with the namespace. Namespace and key will be delimited # with a colon. def normalize_key(key, options) @@ -587,7 +586,6 @@ module ActiveSupport ActiveSupport::Notifications.instrument("cache_#{operation}.active_support", payload) { yield(payload) } end - def log return unless logger && logger.debug? && !silence? logger.debug(yield) -- cgit v1.2.3 From 8300234f9f79b78b78ba1cb90739c776fca8e030 Mon Sep 17 00:00:00 2001 From: Ethan Date: Thu, 18 May 2017 16:06:56 -0700 Subject: lob subscriber should only rescue StandardError, not Exception --- activesupport/lib/active_support/log_subscriber.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'activesupport') diff --git a/activesupport/lib/active_support/log_subscriber.rb b/activesupport/lib/active_support/log_subscriber.rb index e2c4f33565..fd00b4bf83 100644 --- a/activesupport/lib/active_support/log_subscriber.rb +++ b/activesupport/lib/active_support/log_subscriber.rb @@ -81,7 +81,7 @@ module ActiveSupport def finish(name, id, payload) super if logger - rescue Exception => e + rescue => e logger.error "Could not log #{name.inspect} event. #{e.class}: #{e.message} #{e.backtrace}" end -- cgit v1.2.3 From 1c5f5af8c92286ce3f5cb03379098249432faafd Mon Sep 17 00:00:00 2001 From: Ethan Date: Thu, 18 May 2017 16:07:35 -0700 Subject: check that logger is defined in log subscriber rescue before logging --- activesupport/lib/active_support/log_subscriber.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'activesupport') diff --git a/activesupport/lib/active_support/log_subscriber.rb b/activesupport/lib/active_support/log_subscriber.rb index fd00b4bf83..e704e18fb9 100644 --- a/activesupport/lib/active_support/log_subscriber.rb +++ b/activesupport/lib/active_support/log_subscriber.rb @@ -82,7 +82,9 @@ module ActiveSupport def finish(name, id, payload) super if logger rescue => e - logger.error "Could not log #{name.inspect} event. #{e.class}: #{e.message} #{e.backtrace}" + if logger + logger.error "Could not log #{name.inspect} event. #{e.class}: #{e.message} #{e.backtrace}" + end end private -- cgit v1.2.3 From 9b487b939c93e64204401a9c7f7fbb3797876dd7 Mon Sep 17 00:00:00 2001 From: "yuuji.yaginuma" Date: Fri, 26 May 2017 13:03:56 +0900 Subject: Remove unused test class `AlsoDoingNothingTest` was added in cf9be89. It seems that it added to confirm that the test works in the child class of `ActiveSupport::TestCase`. But now basically use `ActiveSupport::TestCase` in test, so I think it is unnecessary. --- activesupport/test/test_case_test.rb | 3 --- 1 file changed, 3 deletions(-) (limited to 'activesupport') diff --git a/activesupport/test/test_case_test.rb b/activesupport/test/test_case_test.rb index af7fc44d66..40dfbe2542 100644 --- a/activesupport/test/test_case_test.rb +++ b/activesupport/test/test_case_test.rb @@ -237,9 +237,6 @@ class AssertDifferenceTest < ActiveSupport::TestCase end end -class AlsoDoingNothingTest < ActiveSupport::TestCase -end - # Setup and teardown callbacks. class SetupAndTeardownTest < ActiveSupport::TestCase setup :reset_callback_record, :foo -- cgit v1.2.3 From 24a864437e845febe91e3646ca008e8dc7f76b56 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Fri, 26 May 2017 20:00:27 +0200 Subject: ActiveSupport::CurrentAttributes provides a thread-isolated attributes singleton (#29180) * Add ActiveSupport::CurrentAttributes to provide a thread-isolated attributes singleton * Need to require first * Move stubs into test namespace. Thus they won't conflict with other Current and Person stubs. * End of the line for you, whitespace! * Support super in attribute methods. Define instance level accessors in an included module such that `super` in an overriden accessor works, akin to Active Model. * Spare users the manual require. Follow the example of concerns, autoload in the top level Active Support file. * Add bidelegation support * Rename #expose to #set. Simpler, clearer * Automatically reset every instance. Skips the need for users to actively embed something that resets their CurrentAttributes instances. * Fix test name; add tangible name value when blank. * Try to ensure we run after a request as well. * Delegate all missing methods to the instance This allows regular `delegate` to serve, so we don't need bidelegate. * Properly test resetting after execution cycle. Also remove the stale puts debugging. * Update documentation to match new autoreset --- activesupport/CHANGELOG.md | 5 + activesupport/lib/active_support.rb | 1 + .../lib/active_support/current_attributes.rb | 190 +++++++++++++++++++++ activesupport/lib/active_support/railtie.rb | 5 + activesupport/test/current_attributes_test.rb | 96 +++++++++++ 5 files changed, 297 insertions(+) create mode 100644 activesupport/lib/active_support/current_attributes.rb create mode 100644 activesupport/test/current_attributes_test.rb (limited to 'activesupport') diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md index bae573cf37..b5db0693fe 100644 --- a/activesupport/CHANGELOG.md +++ b/activesupport/CHANGELOG.md @@ -1,3 +1,8 @@ +* Add ActiveSupport::CurrentAttributes to provide a thread-isolated attributes singleton. + Primary use case is keeping all the per-request attributes easily available to the whole system. + + *DHH* + * Fix implicit coercion calculations with scalars and durations Previously calculations where the scalar is first would be converted to a duration diff --git a/activesupport/lib/active_support.rb b/activesupport/lib/active_support.rb index 03e3ce821a..a667fbb54a 100644 --- a/activesupport/lib/active_support.rb +++ b/activesupport/lib/active_support.rb @@ -32,6 +32,7 @@ module ActiveSupport extend ActiveSupport::Autoload autoload :Concern + autoload :CurrentAttributes autoload :Dependencies autoload :DescendantsTracker autoload :ExecutionWrapper diff --git a/activesupport/lib/active_support/current_attributes.rb b/activesupport/lib/active_support/current_attributes.rb new file mode 100644 index 0000000000..6b859cc6cc --- /dev/null +++ b/activesupport/lib/active_support/current_attributes.rb @@ -0,0 +1,190 @@ +module ActiveSupport + # Abstract super class that provides a thread-isolated attributes singleton, which resets automatically + # before and after reach request. This allows you to keep all the per-request attributes easily + # available to the whole system. + # + # The following full app-like example demonstrates how to use a Current class to + # facilitate easy access to the global, per-request attributes without passing them deeply + # around everywhere: + # + # # app/models/current.rb + # class Current < ActiveSupport::CurrentAttributes + # attribute :account, :user + # attribute :request_id, :user_agent, :ip_address + # + # resets { Time.zone = nil } + # + # def user=(user) + # super + # self.account = user.account + # Time.zone = user.time_zone + # end + # end + # + # # app/controllers/concerns/authentication.rb + # module Authentication + # extend ActiveSupport::Concern + # + # included do + # before_action :authenticate + # end + # + # private + # def authenticate + # if authenticated_user = User.find(cookies.signed[:user_id]) + # Current.user = authenticated_user + # else + # redirect_to new_session_url + # end + # end + # end + # + # # app/controllers/concerns/set_current_request_details.rb + # module SetCurrentRequestDetails + # extend ActiveSupport::Concern + # + # included do + # before_action do + # Current.request_id = request.uuid + # Current.user_agent = request.user_agent + # Current.ip_address = request.ip + # end + # end + # end + # + # class ApplicationController < ActionController::Base + # include Authentication + # include SetCurrentRequestDetails + # end + # + # class MessagesController < ApplicationController + # def create + # Current.account.messages.create(message_params) + # end + # end + # + # class Message < ApplicationRecord + # belongs_to :creator, default: -> { Current.user } + # after_create { |message| Event.create(record: message) } + # end + # + # class Event < ApplicationRecord + # before_create do + # self.request_id = Current.request_id + # self.user_agent = Current.user_agent + # self.ip_address = Current.ip_address + # end + # end + # + # A word of caution: It's easy to overdo a global singleton like Current and tangle your model as a result. + # Current should only be used for a few, top-level globals, like account, user, and request details. + # The attributes stuck in Current should be used by more or less all actions on all requests. If you start + # sticking controller-specific attributes in there, you're going to create a mess. + class CurrentAttributes + include ActiveSupport::Callbacks + define_callbacks :reset + + class << self + # Returns singleton instance for this class in this thread. If none exists, one is created. + def instance + Thread.current[:"current_attributes_for_#{name}"] ||= new.tap do |instance| + current_instances << instance + end + end + + # Declares one or more attributes that will be given both class and instance accessor methods. + def attribute(*names) + generated_attribute_methods.module_eval do + names.each do |name| + define_method(name) do + attributes[name.to_sym] + end + + define_method("#{name}=") do |attribute| + attributes[name.to_sym] = attribute + end + end + end + + names.each do |name| + define_singleton_method(name) do + instance.public_send(name) + end + + define_singleton_method("#{name}=") do |attribute| + instance.public_send("#{name}=", attribute) + end + end + end + + # Calls this block after #reset is called on the instance. Used for resetting external collaborators, like Time.zone. + def resets(&block) + set_callback :reset, :after, &block + end + + delegate :set, :reset, to: :instance + + def reset_all # :nodoc: + current_instances.each(&:reset) + end + + private + def generated_attribute_methods + @generated_attribute_methods ||= Module.new.tap { |mod| include mod } + end + + def current_instances + Thread.current[:current_attributes_instances] ||= [] + end + + def method_missing(name, *args, &block) + # Caches the method definition as a singleton method of the receiver. + # + # By letting #delegate handle it, we avoid an enclosure that'll capture args. + singleton_class.delegate name, to: :instance + + send(name, *args, &block) + end + end + + attr_accessor :attributes + + def initialize + @attributes = {} + end + + # Expose one or more attributes within a block. Old values are returned after the block concludes. + # Example demonstrating the common use of needing to set Current attributes outside the request-cycle: + # + # class Chat::PublicationJob < ApplicationJob + # def perform(attributes, room_number, creator) + # Current.set(person: creator) do + # Chat::Publisher.publish(attributes: attributes, room_number: room_number) + # end + # end + # end + def set(set_attributes) + old_attributes = compute_attributes(set_attributes.keys) + assign_attributes(set_attributes) + yield + ensure + assign_attributes(old_attributes) + end + + # Reset all attributes. Should be called before and after actions, when used as a per-request singleton. + def reset + run_callbacks :reset do + self.attributes = {} + end + end + + private + def assign_attributes(new_attributes) + new_attributes.each { |key, value| public_send("#{key}=", value) } + end + + def compute_attributes(keys) + keys.collect { |key| [ key, public_send(key) ] }.to_h + end + end +end diff --git a/activesupport/lib/active_support/railtie.rb b/activesupport/lib/active_support/railtie.rb index b875875afe..39c83f65a3 100644 --- a/activesupport/lib/active_support/railtie.rb +++ b/activesupport/lib/active_support/railtie.rb @@ -7,6 +7,11 @@ module ActiveSupport config.eager_load_namespaces << ActiveSupport + initializer "active_support.reset_all_current_attributes_instances" do |app| + app.executor.to_run { ActiveSupport::CurrentAttributes.reset_all } + app.executor.to_complete { ActiveSupport::CurrentAttributes.reset_all } + end + initializer "active_support.deprecation_behavior" do |app| if deprecation = app.config.active_support.deprecation ActiveSupport::Deprecation.behavior = deprecation diff --git a/activesupport/test/current_attributes_test.rb b/activesupport/test/current_attributes_test.rb new file mode 100644 index 0000000000..67ef6ef619 --- /dev/null +++ b/activesupport/test/current_attributes_test.rb @@ -0,0 +1,96 @@ +require "abstract_unit" + +class CurrentAttributesTest < ActiveSupport::TestCase + Person = Struct.new(:name, :time_zone) + + class Current < ActiveSupport::CurrentAttributes + attribute :world, :account, :person, :request + delegate :time_zone, to: :person + + resets { Time.zone = "UTC" } + + def account=(account) + super + self.person = "#{account}'s person" + end + + def person=(person) + super + Time.zone = person.try(:time_zone) + end + + def request + "#{super} something" + end + + def intro + "#{person.name}, in #{time_zone}" + end + end + + setup { Current.reset } + + test "read and write attribute" do + Current.world = "world/1" + assert_equal "world/1", Current.world + end + + test "read overwritten attribute method" do + Current.request = "request/1" + assert_equal "request/1 something", Current.request + end + + test "set attribute via overwritten method" do + Current.account = "account/1" + assert_equal "account/1", Current.account + assert_equal "account/1's person", Current.person + end + + test "set auxiliary class via overwritten method" do + Current.person = Person.new("David", "Central Time (US & Canada)") + assert_equal "Central Time (US & Canada)", Time.zone.name + end + + test "resets auxiliary class via callback" do + Current.person = Person.new("David", "Central Time (US & Canada)") + assert_equal "Central Time (US & Canada)", Time.zone.name + + Current.reset + assert_equal "UTC", Time.zone.name + end + + test "set attribute only via scope" do + Current.world = "world/1" + + Current.set(world: "world/2") do + assert_equal "world/2", Current.world + end + + assert_equal "world/1", Current.world + end + + test "set multiple attributes" do + Current.world = "world/1" + Current.account = "account/1" + + Current.set(world: "world/2", account: "account/2") do + assert_equal "world/2", Current.world + assert_equal "account/2", Current.account + end + + assert_equal "world/1", Current.world + assert_equal "account/1", Current.account + end + + test "delegation" do + Current.person = Person.new("David", "Central Time (US & Canada)") + assert_equal "Central Time (US & Canada)", Current.time_zone + assert_equal "Central Time (US & Canada)", Current.instance.time_zone + end + + test "all methods forward to the instance" do + Current.person = Person.new("David", "Central Time (US & Canada)") + assert_equal "David, in Central Time (US & Canada)", Current.intro + assert_equal "David, in Central Time (US & Canada)", Current.instance.intro + end +end -- cgit v1.2.3 From 3a131b6c008eed58fddfa23f2787d9eef657331a Mon Sep 17 00:00:00 2001 From: Kasper Timm Hansen Date: Sat, 27 May 2017 14:34:53 +0200 Subject: [ci skip] Fix spelling that's a bit of an overreach. --- activesupport/lib/active_support/current_attributes.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'activesupport') diff --git a/activesupport/lib/active_support/current_attributes.rb b/activesupport/lib/active_support/current_attributes.rb index 6b859cc6cc..2251f56aef 100644 --- a/activesupport/lib/active_support/current_attributes.rb +++ b/activesupport/lib/active_support/current_attributes.rb @@ -1,6 +1,6 @@ module ActiveSupport # Abstract super class that provides a thread-isolated attributes singleton, which resets automatically - # before and after reach request. This allows you to keep all the per-request attributes easily + # before and after each request. This allows you to keep all the per-request attributes easily # available to the whole system. # # The following full app-like example demonstrates how to use a Current class to -- cgit v1.2.3 From fcc47bcfccc7578aa0414710eecdad006085a911 Mon Sep 17 00:00:00 2001 From: Kasper Timm Hansen Date: Sat, 27 May 2017 14:36:18 +0200 Subject: Use non-raising finder. `find` raises when it can't find a record, so we'll never reach the else. Switch to `find_by` which returns nil when no record can be found. --- activesupport/lib/active_support/current_attributes.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'activesupport') diff --git a/activesupport/lib/active_support/current_attributes.rb b/activesupport/lib/active_support/current_attributes.rb index 2251f56aef..9921241c23 100644 --- a/activesupport/lib/active_support/current_attributes.rb +++ b/activesupport/lib/active_support/current_attributes.rb @@ -31,7 +31,7 @@ module ActiveSupport # # private # def authenticate - # if authenticated_user = User.find(cookies.signed[:user_id]) + # if authenticated_user = User.find_by(id: cookies.signed[:user_id]) # Current.user = authenticated_user # else # redirect_to new_session_url -- cgit v1.2.3 From 3cacbc1ef055b3c1702f640969b5840f612b2995 Mon Sep 17 00:00:00 2001 From: Kasper Timm Hansen Date: Sun, 28 May 2017 10:12:16 +0200 Subject: Remove double Thread.current storage. Since we're generating a key through the class name we can combine the two Thread.current calls into a single hash version. --- activesupport/lib/active_support/current_attributes.rb | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) (limited to 'activesupport') diff --git a/activesupport/lib/active_support/current_attributes.rb b/activesupport/lib/active_support/current_attributes.rb index 9921241c23..837471e97d 100644 --- a/activesupport/lib/active_support/current_attributes.rb +++ b/activesupport/lib/active_support/current_attributes.rb @@ -87,9 +87,7 @@ module ActiveSupport class << self # Returns singleton instance for this class in this thread. If none exists, one is created. def instance - Thread.current[:"current_attributes_for_#{name}"] ||= new.tap do |instance| - current_instances << instance - end + current_instances[name] ||= new end # Declares one or more attributes that will be given both class and instance accessor methods. @@ -125,7 +123,7 @@ module ActiveSupport delegate :set, :reset, to: :instance def reset_all # :nodoc: - current_instances.each(&:reset) + current_instances.each_value(&:reset) end private @@ -134,7 +132,7 @@ module ActiveSupport end def current_instances - Thread.current[:current_attributes_instances] ||= [] + Thread.current[:current_attributes_instances] ||= {} end def method_missing(name, *args, &block) -- cgit v1.2.3 From 85211ea1efd5c249ef53182a0e5e09a56d6f9dd8 Mon Sep 17 00:00:00 2001 From: Kasper Timm Hansen Date: Sun, 28 May 2017 10:47:17 +0200 Subject: Clear all current instances before a reload. If users added an attribute or otherwise changed a CurrentAttributes subclass they'd see exceptions on the next page load. Because `ActiveSupport::CurrentAttributes.current_instances` would keep references to the old instances from the previous request. We can fix this by clearing out the `current_attributes` before we unload constants. Then any change to the model can be autoloaded again since its slot isn't taken by an old instance. We'll still have to call reset before we clear so external collaborators, like Time.zone, won't linger with their current value throughout other code. --- activesupport/lib/active_support/current_attributes.rb | 5 +++++ activesupport/lib/active_support/railtie.rb | 5 +++-- 2 files changed, 8 insertions(+), 2 deletions(-) (limited to 'activesupport') diff --git a/activesupport/lib/active_support/current_attributes.rb b/activesupport/lib/active_support/current_attributes.rb index 837471e97d..872b0663c7 100644 --- a/activesupport/lib/active_support/current_attributes.rb +++ b/activesupport/lib/active_support/current_attributes.rb @@ -126,6 +126,11 @@ module ActiveSupport current_instances.each_value(&:reset) end + def clear_all # :nodoc: + reset_all + current_instances.clear + end + private def generated_attribute_methods @generated_attribute_methods ||= Module.new.tap { |mod| include mod } diff --git a/activesupport/lib/active_support/railtie.rb b/activesupport/lib/active_support/railtie.rb index 39c83f65a3..1b4ecf4d72 100644 --- a/activesupport/lib/active_support/railtie.rb +++ b/activesupport/lib/active_support/railtie.rb @@ -8,8 +8,9 @@ module ActiveSupport config.eager_load_namespaces << ActiveSupport initializer "active_support.reset_all_current_attributes_instances" do |app| - app.executor.to_run { ActiveSupport::CurrentAttributes.reset_all } - app.executor.to_complete { ActiveSupport::CurrentAttributes.reset_all } + app.reloader.before_class_unload { ActiveSupport::CurrentAttributes.clear_all } + app.executor.to_run { ActiveSupport::CurrentAttributes.reset_all } + app.executor.to_complete { ActiveSupport::CurrentAttributes.reset_all } end initializer "active_support.deprecation_behavior" do |app| -- cgit v1.2.3 From 1c275d812f35f53f93cd96184a4f319983766cc5 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 29 May 2017 18:01:50 +0200 Subject: Add option for class_attribute default (#29270) * Allow a default value to be declared for class_attribute * Convert to using class_attribute default rather than explicit setter * Removed instance_accessor option by mistake * False is a valid default value * Documentation --- activesupport/CHANGELOG.md | 11 +++++++++++ activesupport/lib/active_support/callbacks.rb | 3 +-- .../lib/active_support/core_ext/class/attribute.rb | 13 +++++++++++-- activesupport/lib/active_support/reloader.rb | 7 ++----- activesupport/lib/active_support/rescuable.rb | 3 +-- activesupport/test/core_ext/class/attribute_test.rb | 10 +++++++++- 6 files changed, 35 insertions(+), 12 deletions(-) (limited to 'activesupport') diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md index af70a81414..bb578d81dc 100644 --- a/activesupport/CHANGELOG.md +++ b/activesupport/CHANGELOG.md @@ -1,3 +1,14 @@ +* Add default option to class_attribute. Before: + + class_attribute :settings + self.settings = {} + + Now: + + class_attribute :settings, default: {} + + *DHH* + * `#singularize` and `#pluralize` now respect uncountables for the specified locale. *Eilis Hamilton* diff --git a/activesupport/lib/active_support/callbacks.rb b/activesupport/lib/active_support/callbacks.rb index d771cab68b..ddfa91a342 100644 --- a/activesupport/lib/active_support/callbacks.rb +++ b/activesupport/lib/active_support/callbacks.rb @@ -62,8 +62,7 @@ module ActiveSupport included do extend ActiveSupport::DescendantsTracker - class_attribute :__callbacks, instance_writer: false - self.__callbacks ||= {} + class_attribute :__callbacks, instance_writer: false, default: {} end CALLBACK_FILTER_TYPES = [:before, :after, :around] diff --git a/activesupport/lib/active_support/core_ext/class/attribute.rb b/activesupport/lib/active_support/core_ext/class/attribute.rb index ba422f9071..8caddcd5c3 100644 --- a/activesupport/lib/active_support/core_ext/class/attribute.rb +++ b/activesupport/lib/active_support/core_ext/class/attribute.rb @@ -68,11 +68,16 @@ class Class # object.setting = false # => NoMethodError # # To opt out of both instance methods, pass instance_accessor: false. + # + # To set a default value for the attribute, pass default:, like so: + # + # class_attribute :settings, default: {} def class_attribute(*attrs) options = attrs.extract_options! - instance_reader = options.fetch(:instance_accessor, true) && options.fetch(:instance_reader, true) - instance_writer = options.fetch(:instance_accessor, true) && options.fetch(:instance_writer, true) + instance_reader = options.fetch(:instance_accessor, true) && options.fetch(:instance_reader, true) + instance_writer = options.fetch(:instance_accessor, true) && options.fetch(:instance_writer, true) instance_predicate = options.fetch(:instance_predicate, true) + default_value = options.fetch(:default, nil) attrs.each do |name| remove_possible_singleton_method(name) @@ -123,6 +128,10 @@ class Class remove_possible_method "#{name}=" attr_writer name end + + unless default_value.nil? + self.send("#{name}=", default_value) + end end end end diff --git a/activesupport/lib/active_support/reloader.rb b/activesupport/lib/active_support/reloader.rb index 121c621751..9558146201 100644 --- a/activesupport/lib/active_support/reloader.rb +++ b/activesupport/lib/active_support/reloader.rb @@ -69,11 +69,8 @@ module ActiveSupport end end - class_attribute :executor - class_attribute :check - - self.executor = Executor - self.check = lambda { false } + class_attribute :executor, default: Executor + class_attribute :check, default: lambda { false } def self.check! # :nodoc: @should_reload ||= check.call diff --git a/activesupport/lib/active_support/rescuable.rb b/activesupport/lib/active_support/rescuable.rb index 12ec8bf1b8..826832ba7d 100644 --- a/activesupport/lib/active_support/rescuable.rb +++ b/activesupport/lib/active_support/rescuable.rb @@ -8,8 +8,7 @@ module ActiveSupport extend Concern included do - class_attribute :rescue_handlers - self.rescue_handlers = [] + class_attribute :rescue_handlers, default: [] end module ClassMethods diff --git a/activesupport/test/core_ext/class/attribute_test.rb b/activesupport/test/core_ext/class/attribute_test.rb index 5a9ec78cc1..f16043c612 100644 --- a/activesupport/test/core_ext/class/attribute_test.rb +++ b/activesupport/test/core_ext/class/attribute_test.rb @@ -3,7 +3,11 @@ require "active_support/core_ext/class/attribute" class ClassAttributeTest < ActiveSupport::TestCase def setup - @klass = Class.new { class_attribute :setting } + @klass = Class.new do + class_attribute :setting + class_attribute :timeout, default: 5 + end + @sub = Class.new(@klass) end @@ -12,6 +16,10 @@ class ClassAttributeTest < ActiveSupport::TestCase assert_nil @sub.setting end + test "custom default" do + assert_equal 5, @klass.timeout + end + test "inheritable" do @klass.setting = 1 assert_equal 1, @sub.setting -- cgit v1.2.3 From 0a9522a8b316171d083623cd9dd50f2aa21efb2c Mon Sep 17 00:00:00 2001 From: Ryuta Kamizono Date: Tue, 30 May 2017 05:53:07 +0900 Subject: Add missing "not" in the doc for `assert_no_changes` [ci skip] --- activesupport/lib/active_support/testing/assertions.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'activesupport') diff --git a/activesupport/lib/active_support/testing/assertions.rb b/activesupport/lib/active_support/testing/assertions.rb index 28cf2953bf..28e1df8870 100644 --- a/activesupport/lib/active_support/testing/assertions.rb +++ b/activesupport/lib/active_support/testing/assertions.rb @@ -167,7 +167,7 @@ module ActiveSupport retval end - # Assertion that the result of evaluating an expression is changed before + # Assertion that the result of evaluating an expression is not changed before # and after invoking the passed in block. # # assert_no_changes 'Status.all_good?' do -- cgit v1.2.3 From c47e6ffbd55a76b15e640d10548b73070fd37c94 Mon Sep 17 00:00:00 2001 From: Jon Moss Date: Mon, 29 May 2017 19:44:39 -0400 Subject: Add backticks [ci skip] --- activesupport/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'activesupport') diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md index bb578d81dc..1cf4d30898 100644 --- a/activesupport/CHANGELOG.md +++ b/activesupport/CHANGELOG.md @@ -13,7 +13,7 @@ *Eilis Hamilton* -* Add ActiveSupport::CurrentAttributes to provide a thread-isolated attributes singleton. +* Add `ActiveSupport::CurrentAttributes` to provide a thread-isolated attributes singleton. Primary use case is keeping all the per-request attributes easily available to the whole system. *DHH* -- cgit v1.2.3 From 9050f6ee0133e9088f914a77bb65108a89f49dfd Mon Sep 17 00:00:00 2001 From: Jon Moss Date: Mon, 29 May 2017 19:46:01 -0400 Subject: Fix indentation + remove blank line [ci skip] --- activesupport/CHANGELOG.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'activesupport') diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md index 1cf4d30898..fd07187e15 100644 --- a/activesupport/CHANGELOG.md +++ b/activesupport/CHANGELOG.md @@ -1,11 +1,11 @@ * Add default option to class_attribute. Before: - class_attribute :settings - self.settings = {} + class_attribute :settings + self.settings = {} Now: - class_attribute :settings, default: {} + class_attribute :settings, default: {} *DHH* @@ -15,7 +15,7 @@ * Add `ActiveSupport::CurrentAttributes` to provide a thread-isolated attributes singleton. Primary use case is keeping all the per-request attributes easily available to the whole system. - + *DHH* * Fix implicit coercion calculations with scalars and durations -- cgit v1.2.3 From 3fbe657e9b65814b3837fd13628e7a812dc0a0ea Mon Sep 17 00:00:00 2001 From: Shota Iguchi Date: Tue, 30 May 2017 20:13:29 +0900 Subject: Add next occur and previous occurred day of week API (#26600) --- activesupport/CHANGELOG.md | 4 ++++ .../core_ext/date_and_time/calculations.rb | 16 ++++++++++++++++ activesupport/test/core_ext/date_time_ext_test.rb | 22 ++++++++++++++++++++++ 3 files changed, 42 insertions(+) (limited to 'activesupport') diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md index fd07187e15..4c3eded38a 100644 --- a/activesupport/CHANGELOG.md +++ b/activesupport/CHANGELOG.md @@ -1,3 +1,7 @@ +* Add `Date#prev_occurring` and `Date#next_occurring` to return specified next/previous occurring day of week. + + *Shota Iguchi* + * Add default option to class_attribute. Before: class_attribute :settings 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 f2ba7fdda5..e2e1d3e359 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 @@ -320,6 +320,22 @@ module DateAndTime beginning_of_year..end_of_year end + # Returns specific next occurring day of week + def next_occurring(day_of_week) + current_day_number = wday != 0 ? wday - 1 : 6 + from_now = DAYS_INTO_WEEK.fetch(day_of_week) - current_day_number + from_now += 7 unless from_now > 0 + since(from_now.days) + end + + # Returns specific previous occurring day of week + def prev_occurring(day_of_week) + current_day_number = wday != 0 ? wday - 1 : 6 + ago = current_day_number - DAYS_INTO_WEEK.fetch(day_of_week) + ago += 7 unless ago > 0 + ago(ago.days) + end + private def first_hour(date_or_time) date_or_time.acts_like?(:time) ? date_or_time.beginning_of_day : date_or_time diff --git a/activesupport/test/core_ext/date_time_ext_test.rb b/activesupport/test/core_ext/date_time_ext_test.rb index be7c14e9b4..276fa2bfd3 100644 --- a/activesupport/test/core_ext/date_time_ext_test.rb +++ b/activesupport/test/core_ext/date_time_ext_test.rb @@ -28,6 +28,28 @@ class DateTimeExtCalculationsTest < ActiveSupport::TestCase end end + def test_next_occur + datetime = DateTime.new(2016, 9, 24, 0, 0) # saturday + assert_equal datetime.next_occurring(:monday), datetime.since(2.days) + assert_equal datetime.next_occurring(:tuesday), datetime.since(3.days) + assert_equal datetime.next_occurring(:wednesday), datetime.since(4.days) + assert_equal datetime.next_occurring(:thursday), datetime.since(5.days) + assert_equal datetime.next_occurring(:friday), datetime.since(6.days) + assert_equal datetime.next_occurring(:saturday), datetime.since(1.week) + assert_equal datetime.next_occurring(:sunday), datetime.since(1.day) + end + + def test_prev_occur + datetime = DateTime.new(2016, 9, 24, 0, 0) # saturday + assert_equal datetime.prev_occurring(:monday), datetime.ago(5.days) + assert_equal datetime.prev_occurring(:tuesday), datetime.ago(4.days) + assert_equal datetime.prev_occurring(:wednesday), datetime.ago(3.days) + assert_equal datetime.prev_occurring(:thursday), datetime.ago(2.days) + assert_equal datetime.prev_occurring(:friday), datetime.ago(1.day) + assert_equal datetime.prev_occurring(:saturday), datetime.ago(1.week) + assert_equal datetime.prev_occurring(:sunday), datetime.ago(6.days) + end + def test_readable_inspect datetime = DateTime.new(2005, 2, 21, 14, 30, 0) assert_equal "Mon, 21 Feb 2005 14:30:00 +0000", datetime.readable_inspect -- cgit v1.2.3 From a5b0c60714e1e8d8c182af830a26e1c7c884271d Mon Sep 17 00:00:00 2001 From: Genadi Samokovarov Date: Tue, 30 May 2017 16:16:19 +0300 Subject: Implement mattr_acessor :default option --- activesupport/CHANGELOG.md | 9 +++++ .../core_ext/module/attribute_accessors.rb | 43 ++++++++++------------ .../core_ext/module/attribute_accessor_test.rb | 38 ++++++++++++++++++- 3 files changed, 65 insertions(+), 25 deletions(-) (limited to 'activesupport') diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md index 4c3eded38a..c4c6e139ff 100644 --- a/activesupport/CHANGELOG.md +++ b/activesupport/CHANGELOG.md @@ -1,3 +1,12 @@ +* Add default option to module and class attribute accessors. + + mattr_accessor :settings, default: {} + + Works for `mattr_reader`, `mattr_writer`, `cattr_accessor`, `cattr_reader`, + and `cattr_writer` as well. + + *Genadi Samokovarov* + * Add `Date#prev_occurring` and `Date#next_occurring` to return specified next/previous occurring day of week. *Shota Iguchi* 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 2c24081eb9..9244cfa157 100644 --- a/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb +++ b/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb @@ -38,13 +38,10 @@ class Module # # Person.new.hair_colors # => NoMethodError # - # - # Also, you can pass a block to set up the attribute with a default value. + # You can set a default value for the attribute. # # module HairColors - # mattr_reader :hair_colors do - # [:brown, :black, :blonde, :red] - # end + # mattr_reader :hair_colors, default: [:brown, :black, :blonde, :red] # end # # class Person @@ -52,8 +49,7 @@ class Module # end # # Person.new.hair_colors # => [:brown, :black, :blonde, :red] - def mattr_reader(*syms) - options = syms.extract_options! + def mattr_reader(*syms, instance_reader: true, instance_accessor: true, default: nil) syms.each do |sym| raise NameError.new("invalid attribute name: #{sym}") unless /\A[_A-Za-z]\w*\z/.match?(sym) class_eval(<<-EOS, __FILE__, __LINE__ + 1) @@ -64,14 +60,16 @@ class Module end EOS - unless options[:instance_reader] == false || options[:instance_accessor] == false + if instance_reader && instance_accessor class_eval(<<-EOS, __FILE__, __LINE__ + 1) def #{sym} @@#{sym} end EOS end - class_variable_set("@@#{sym}", yield) if block_given? + + sym_default_value = (block_given? && default.nil?) ? yield : default + class_variable_set("@@#{sym}", sym_default_value) unless sym_default_value.nil? end end alias :cattr_reader :mattr_reader @@ -107,12 +105,10 @@ class Module # # Person.new.hair_colors = [:blonde, :red] # => NoMethodError # - # Also, you can pass a block to set up the attribute with a default value. + # You can set a default value for the attribute. # # module HairColors - # mattr_writer :hair_colors do - # [:brown, :black, :blonde, :red] - # end + # mattr_writer :hair_colors, default: [:brown, :black, :blonde, :red] # end # # class Person @@ -120,8 +116,7 @@ class Module # end # # Person.class_variable_get("@@hair_colors") # => [:brown, :black, :blonde, :red] - def mattr_writer(*syms) - options = syms.extract_options! + def mattr_writer(*syms, instance_writer: true, instance_accessor: true, default: nil) syms.each do |sym| raise NameError.new("invalid attribute name: #{sym}") unless /\A[_A-Za-z]\w*\z/.match?(sym) class_eval(<<-EOS, __FILE__, __LINE__ + 1) @@ -132,14 +127,16 @@ class Module end EOS - unless options[:instance_writer] == false || options[:instance_accessor] == false + if instance_writer && instance_accessor class_eval(<<-EOS, __FILE__, __LINE__ + 1) def #{sym}=(obj) @@#{sym} = obj end EOS end - send("#{sym}=", yield) if block_given? + + sym_default_value = (block_given? && default.nil?) ? yield : default + send("#{sym}=", sym_default_value) unless sym_default_value.nil? end end alias :cattr_writer :mattr_writer @@ -197,12 +194,10 @@ class Module # Person.new.hair_colors = [:brown] # => NoMethodError # Person.new.hair_colors # => NoMethodError # - # Also you can pass a block to set up the attribute with a default value. + # You can set a default value for the attribute. # # module HairColors - # mattr_accessor :hair_colors do - # [:brown, :black, :blonde, :red] - # end + # mattr_accessor :hair_colors, default: [:brown, :black, :blonde, :red] # end # # class Person @@ -210,9 +205,9 @@ class Module # end # # Person.class_variable_get("@@hair_colors") # => [:brown, :black, :blonde, :red] - def mattr_accessor(*syms, &blk) - mattr_reader(*syms, &blk) - mattr_writer(*syms) + def mattr_accessor(*syms, instance_reader: true, instance_writer: true, instance_accessor: true, default: nil, &blk) + mattr_reader(*syms, instance_reader: instance_reader, instance_accessor: instance_accessor, default: default, &blk) + mattr_writer(*syms, instance_writer: instance_writer, instance_accessor: instance_accessor, default: default) end alias :cattr_accessor :mattr_accessor end diff --git a/activesupport/test/core_ext/module/attribute_accessor_test.rb b/activesupport/test/core_ext/module/attribute_accessor_test.rb index 464a000d59..9b185e9381 100644 --- a/activesupport/test/core_ext/module/attribute_accessor_test.rb +++ b/activesupport/test/core_ext/module/attribute_accessor_test.rb @@ -12,7 +12,14 @@ class ModuleAttributeAccessorTest < ActiveSupport::TestCase cattr_accessor(:defa) { "default_accessor_value" } cattr_reader(:defr) { "default_reader_value" } cattr_writer(:defw) { "default_writer_value" } + cattr_accessor(:deff) { false } cattr_accessor(:quux) { :quux } + + cattr_accessor :def_accessor, default: "default_accessor_value" + cattr_reader :def_reader, default: "default_reader_value" + cattr_writer :def_writer, default: "default_writer_value" + cattr_accessor :def_false, default: false + cattr_accessor(:def_priority, default: false) { :no_priority } end @class = Class.new @class.instance_eval { include m } @@ -24,6 +31,21 @@ class ModuleAttributeAccessorTest < ActiveSupport::TestCase assert_nil @object.foo end + def test_mattr_default_keyword_arguments + assert_equal "default_accessor_value", @module.def_accessor + assert_equal "default_reader_value", @module.def_reader + assert_equal "default_writer_value", @module.class_variable_get(:@@def_writer) + end + + def test_mattr_can_default_to_false + assert_equal false, @module.def_false + assert_equal false, @module.deff + end + + def test_mattr_default_priority + assert_equal false, @module.def_priority + end + def test_should_set_mattr_value @module.foo = :test assert_equal :test, @object.foo @@ -91,9 +113,23 @@ class ModuleAttributeAccessorTest < ActiveSupport::TestCase assert_equal "default_writer_value", @module.class_variable_get("@@defw") end - def test_should_not_invoke_default_value_block_multiple_times + def test_method_invocation_should_not_invoke_the_default_block count = 0 + @module.cattr_accessor(:defcount) { count += 1 } + assert_equal 1, count + assert_no_difference "count" do + @module.defcount + end + end + + def test_declaring_multiple_attributes_at_once_invokes_the_block_multiple_times + count = 0 + + @module.cattr_accessor(:defn1, :defn2) { count += 1 } + + assert_equal 1, @module.defn1 + assert_equal 2, @module.defn2 end end -- cgit v1.2.3 From b6b0c99ff3e8ace3f42813154dbe4b8ad6a98e6c Mon Sep 17 00:00:00 2001 From: Genadi Samokovarov Date: Wed, 31 May 2017 12:16:20 +0300 Subject: Use mattr_accessor default: option throughout the project --- .../core_ext/date_and_time/compatibility.rb | 2 +- activesupport/lib/active_support/dependencies.rb | 33 ++++++++-------------- activesupport/lib/active_support/log_subscriber.rb | 3 +- activesupport/lib/active_support/logger_silence.rb | 3 +- 4 files changed, 14 insertions(+), 27 deletions(-) (limited to 'activesupport') diff --git a/activesupport/lib/active_support/core_ext/date_and_time/compatibility.rb b/activesupport/lib/active_support/core_ext/date_and_time/compatibility.rb index ab80392460..2d45e16546 100644 --- a/activesupport/lib/active_support/core_ext/date_and_time/compatibility.rb +++ b/activesupport/lib/active_support/core_ext/date_and_time/compatibility.rb @@ -9,6 +9,6 @@ module DateAndTime # of the receiver. For backwards compatibility we're overriding # this behavior, but new apps will have an initializer that sets # this to true, because the new behavior is preferred. - mattr_accessor(:preserve_timezone, instance_writer: false) { false } + mattr_accessor :preserve_timezone, instance_writer: false, default: false end end diff --git a/activesupport/lib/active_support/dependencies.rb b/activesupport/lib/active_support/dependencies.rb index e125b657f2..3cd8f3d0ac 100644 --- a/activesupport/lib/active_support/dependencies.rb +++ b/activesupport/lib/active_support/dependencies.rb @@ -18,8 +18,7 @@ module ActiveSupport #:nodoc: module Dependencies #:nodoc: extend self - mattr_accessor :interlock - self.interlock = Interlock.new + mattr_accessor :interlock, default: Interlock.new # :doc: @@ -46,46 +45,37 @@ module ActiveSupport #:nodoc: # :nodoc: # Should we turn on Ruby warnings on the first load of dependent files? - mattr_accessor :warnings_on_first_load - self.warnings_on_first_load = false + mattr_accessor :warnings_on_first_load, default: false # All files ever loaded. - mattr_accessor :history - self.history = Set.new + mattr_accessor :history, default: Set.new # All files currently loaded. - mattr_accessor :loaded - self.loaded = Set.new + mattr_accessor :loaded, default: Set.new # Stack of files being loaded. - mattr_accessor :loading - self.loading = [] + mattr_accessor :loading, default: [] # Should we load files or require them? - mattr_accessor :mechanism - self.mechanism = ENV["NO_RELOAD"] ? :require : :load + mattr_accessor :mechanism, default: ENV["NO_RELOAD"] ? :require : :load # The set of directories from which we may automatically load files. Files # under these directories will be reloaded on each request in development mode, # unless the directory also appears in autoload_once_paths. - mattr_accessor :autoload_paths - self.autoload_paths = [] + mattr_accessor :autoload_paths, default: [] # The set of directories from which automatically loaded constants are loaded # only once. All directories in this set must also be present in +autoload_paths+. - mattr_accessor :autoload_once_paths - self.autoload_once_paths = [] + mattr_accessor :autoload_once_paths, default: [] # An array of qualified constant names that have been loaded. Adding a name # to this array will cause it to be unloaded the next time Dependencies are # cleared. - mattr_accessor :autoloaded_constants - self.autoloaded_constants = [] + mattr_accessor :autoloaded_constants, default: [] # An array of constant names that need to be unloaded on every request. Used # to allow arbitrary constants to be marked for unloading. - mattr_accessor :explicitly_unloadable_constants - self.explicitly_unloadable_constants = [] + mattr_accessor :explicitly_unloadable_constants, default: [] # The WatchStack keeps a stack of the modules being watched as files are # loaded. If a file in the process of being loaded (parent.rb) triggers the @@ -175,8 +165,7 @@ module ActiveSupport #:nodoc: end # An internal stack used to record which constants are loaded by any block. - mattr_accessor :constant_watch_stack - self.constant_watch_stack = WatchStack.new + mattr_accessor :constant_watch_stack, default: WatchStack.new # Module includes this module. module ModuleConstMissing #:nodoc: diff --git a/activesupport/lib/active_support/log_subscriber.rb b/activesupport/lib/active_support/log_subscriber.rb index e2c4f33565..e533c6662e 100644 --- a/activesupport/lib/active_support/log_subscriber.rb +++ b/activesupport/lib/active_support/log_subscriber.rb @@ -49,8 +49,7 @@ module ActiveSupport CYAN = "\e[36m" WHITE = "\e[37m" - mattr_accessor :colorize_logging - self.colorize_logging = true + mattr_accessor :colorize_logging, default: true class << self def logger diff --git a/activesupport/lib/active_support/logger_silence.rb b/activesupport/lib/active_support/logger_silence.rb index 632994cf50..9c64afaaca 100644 --- a/activesupport/lib/active_support/logger_silence.rb +++ b/activesupport/lib/active_support/logger_silence.rb @@ -6,8 +6,7 @@ module LoggerSilence extend ActiveSupport::Concern included do - cattr_accessor :silencer - self.silencer = true + cattr_accessor :silencer, default: true end # Silences the logger for the duration of the block. -- cgit v1.2.3 From 3168da0af75bd05082be4909deaa1e3eb1e78e27 Mon Sep 17 00:00:00 2001 From: Vipul A M Date: Sun, 4 Jun 2017 15:42:16 +0530 Subject: Don't create extra assignment, just return --- activesupport/lib/active_support/number_helper/rounding_helper.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'activesupport') diff --git a/activesupport/lib/active_support/number_helper/rounding_helper.rb b/activesupport/lib/active_support/number_helper/rounding_helper.rb index d9644df17d..63b48444a6 100644 --- a/activesupport/lib/active_support/number_helper/rounding_helper.rb +++ b/activesupport/lib/active_support/number_helper/rounding_helper.rb @@ -40,11 +40,11 @@ module ActiveSupport def convert_to_decimal(number) case number when Float, String - number = BigDecimal(number.to_s) + BigDecimal(number.to_s) when Rational - number = BigDecimal(number, digit_count(number.to_i) + precision) + BigDecimal(number, digit_count(number.to_i) + precision) else - number = number.to_d + number.to_d end end -- cgit v1.2.3 From c27991fc6387444070ec6a229fad37e1b413f04c Mon Sep 17 00:00:00 2001 From: Ryuta Kamizono Date: Tue, 6 Jun 2017 12:03:37 +0900 Subject: Fix indentation + Add backticks [ci skip] --- activesupport/CHANGELOG.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) (limited to 'activesupport') diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md index c4c6e139ff..fc1e5516f8 100644 --- a/activesupport/CHANGELOG.md +++ b/activesupport/CHANGELOG.md @@ -1,6 +1,6 @@ * Add default option to module and class attribute accessors. - mattr_accessor :settings, default: {} + mattr_accessor :settings, default: {} Works for `mattr_reader`, `mattr_writer`, `cattr_accessor`, `cattr_reader`, and `cattr_writer` as well. @@ -11,7 +11,9 @@ *Shota Iguchi* -* Add default option to class_attribute. Before: +* Add default option to `class_attribute`. + + Before: class_attribute :settings self.settings = {} -- cgit v1.2.3 From 2b96d5822bfe407be7589e293f3265c0c7a6726c Mon Sep 17 00:00:00 2001 From: Jeremy Daer Date: Tue, 6 Jun 2017 16:39:20 -0700 Subject: Cache: write_multi (#29366) Rails.cache.write_multi foo: 'bar', baz: 'qux' Plus faster `fetch_multi` with stores that implement `write_multi_entries`. Keys that aren't found may be written to the cache store in one shot instead of separate writes. The default implementation simply calls `write_entry` for each entry. Stores may override if they're capable of one-shot bulk writes, like Redis `MSET`. --- activesupport/CHANGELOG.md | 14 ++++++++ activesupport/lib/active_support/cache.rb | 34 ++++++++++++++---- activesupport/test/caching_test.rb | 58 +++++++++++++++++++++++++++++++ 3 files changed, 100 insertions(+), 6 deletions(-) (limited to 'activesupport') diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md index fc1e5516f8..d1d61ac8d7 100644 --- a/activesupport/CHANGELOG.md +++ b/activesupport/CHANGELOG.md @@ -1,3 +1,17 @@ +* Cache: `write_multi` + + Rails.cache.write_multi foo: 'bar', baz: 'qux' + + Plus faster fetch_multi with stores that implement `write_multi_entries`. + Keys that aren't found may be written to the cache store in one shot + instead of separate writes. + + The default implementation simply calls `write_entry` for each entry. + Stores may override if they're capable of one-shot bulk writes, like + Redis `MSET`. + + *Jeremy Daer* + * Add default option to module and class attribute accessors. mattr_accessor :settings, default: {} diff --git a/activesupport/lib/active_support/cache.rb b/activesupport/lib/active_support/cache.rb index a1093a2e23..fa487060e2 100644 --- a/activesupport/lib/active_support/cache.rb +++ b/activesupport/lib/active_support/cache.rb @@ -373,6 +373,19 @@ module ActiveSupport results end + # Cache Storage API to write multiple values at once. + def write_multi(hash, options = nil) + options = merged_options(options) + + instrument :write_multi, hash, options do |payload| + entries = hash.each_with_object({}) do |(name, value), memo| + memo[normalize_key(name, options)] = Entry.new(value, options.merge(version: normalize_version(name, options))) + end + + write_multi_entries entries, options + end + end + # Fetches data from the cache, using the given keys. If there is data in # the cache with the given keys, then that data is returned. Otherwise, # the supplied block is called for each key for which there was no data, @@ -397,14 +410,15 @@ module ActiveSupport options = names.extract_options! options = merged_options(options) - results = read_multi(*names, options) - names.each_with_object({}) do |name, memo| - memo[name] = results.fetch(name) do - value = yield name - write(name, value, options) - value + read_multi(*names, options).tap do |results| + writes = {} + + (names - results.keys).each do |name| + results[name] = writes[name] = yield(name) end + + write_multi writes, options end end @@ -521,6 +535,14 @@ module ActiveSupport raise NotImplementedError.new end + # Writes multiple entries to the cache implementation. Subclasses MAY + # implement this method. + def write_multi_entries(hash, options) + hash.each do |key, entry| + write_entry key, entry, options + end + end + # Deletes an entry from the cache implementation. Subclasses must # implement this method. def delete_entry(key, options) diff --git a/activesupport/test/caching_test.rb b/activesupport/test/caching_test.rb index f53b98c73e..f2f8c58111 100644 --- a/activesupport/test/caching_test.rb +++ b/activesupport/test/caching_test.rb @@ -1299,3 +1299,61 @@ class CacheEntryTest < ActiveSupport::TestCase assert_equal value.bytesize, entry.size end end + +class CacheStoreWriteMultiEntriesStoreProviderInterfaceTest < ActiveSupport::TestCase + setup do + @cache = ActiveSupport::Cache.lookup_store(:null_store) + end + + test "fetch_multi uses write_multi_entries store provider interface" do + assert_called_with(@cache, :write_multi_entries) do + @cache.fetch_multi "a", "b", "c" do |key| + key * 2 + end + end + end +end + +class CacheStoreWriteMultiInstrumentationTest < ActiveSupport::TestCase + setup do + @cache = ActiveSupport::Cache.lookup_store(:null_store) + end + + test "instrumentation" do + writes = { "a" => "aa", "b" => "bb" } + + events = with_instrumentation "write_multi" do + @cache.write_multi(writes) + end + + assert_equal %w[ cache_write_multi.active_support ], events.map(&:name) + assert_nil events[0].payload[:super_operation] + assert_equal({ "a" => "aa", "b" => "bb" }, events[0].payload[:key]) + end + + test "instrumentation with fetch_multi as super operation" do + skip "fetch_multi isn't instrumented yet" + + events = with_instrumentation "write_multi" do + @cache.fetch_multi("a", "b") { |key| key * 2 } + end + + assert_equal %w[ cache_write_multi.active_support ], events.map(&:name) + assert_nil events[0].payload[:super_operation] + assert !events[0].payload[:hit] + end + + private + def with_instrumentation(method) + event_name = "cache_#{method}.active_support" + + [].tap do |events| + ActiveSupport::Notifications.subscribe event_name do |*args| + events << ActiveSupport::Notifications::Event.new(*args) + end + yield + end + ensure + ActiveSupport::Notifications.unsubscribe event_name + end +end -- cgit v1.2.3 From d93317534ca6350ae9f8a9f285250e5b63728ba3 Mon Sep 17 00:00:00 2001 From: "yuuji.yaginuma" Date: Thu, 8 Jun 2017 08:43:27 +0900 Subject: Remove unreachable code `Time.find_zone!` raise `ArgumentError` if invalid value is specified. https://github.com/rails/rails/blob/379a0b42daf0d8e14130db7fd886d05d8d88e3f2/activesupport/lib/active_support/core_ext/time/zones.rb#L97..L99 Therefore, the return value never becomes nil. --- activesupport/lib/active_support/railtie.rb | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) (limited to 'activesupport') diff --git a/activesupport/lib/active_support/railtie.rb b/activesupport/lib/active_support/railtie.rb index 1b4ecf4d72..af1d5bd615 100644 --- a/activesupport/lib/active_support/railtie.rb +++ b/activesupport/lib/active_support/railtie.rb @@ -28,14 +28,7 @@ module ActiveSupport raise e.exception "tzinfo-data is not present. Please add gem 'tzinfo-data' to your Gemfile and run bundle install" end require "active_support/core_ext/time/zones" - zone_default = Time.find_zone!(app.config.time_zone) - - unless zone_default - raise "Value assigned to config.time_zone not recognized. " \ - 'Run "rake time:zones:all" for a time zone names list.' - end - - Time.zone_default = zone_default + Time.zone_default = Time.find_zone!(app.config.time_zone) end # Sets the default week start -- cgit v1.2.3 From ddea3164250ed5f3886f07cbbc01727fd6dff99c Mon Sep 17 00:00:00 2001 From: Jeremy Daer Date: Sat, 10 Jun 2017 01:07:40 -0700 Subject: Split up the cache test suite so it's easier to understand and extend (#29404) Split up the caching tests as prep for adding a new cache store. Slices the mega test/caching_test.rb into behavior modules, concrete store tests, and cross-cutting store tests. Considering moving cache store behavior modules into lib/ so they may be used for acceptance testing by third parties. --- activesupport/test/cache/behaviors.rb | 7 + .../cache/behaviors/autoloading_cache_behavior.rb | 41 + .../behaviors/cache_delete_matched_behavior.rb | 13 + .../cache_increment_decrement_behavior.rb | 21 + .../test/cache/behaviors/cache_store_behavior.rb | 329 +++++ .../behaviors/cache_store_version_behavior.rb | 86 ++ .../cache/behaviors/encoded_key_cache_behavior.rb | 34 + .../test/cache/behaviors/local_cache_behavior.rb | 111 ++ activesupport/test/cache/cache_entry_test.rb | 28 + activesupport/test/cache/cache_key_test.rb | 88 ++ .../test/cache/cache_store_logger_test.rb | 34 + .../test/cache/cache_store_namespace_test.rb | 38 + .../test/cache/cache_store_setting_test.rb | 66 + .../test/cache/cache_store_write_multi_test.rb | 60 + .../test/cache/local_cache_middleware_test.rb | 61 + activesupport/test/cache/stores/file_store_test.rb | 128 ++ .../test/cache/stores/mem_cache_store_test.rb | 74 ++ .../test/cache/stores/memory_store_test.rb | 107 ++ activesupport/test/cache/stores/null_store_test.rb | 57 + activesupport/test/caching_test.rb | 1359 -------------------- 20 files changed, 1383 insertions(+), 1359 deletions(-) create mode 100644 activesupport/test/cache/behaviors.rb create mode 100644 activesupport/test/cache/behaviors/autoloading_cache_behavior.rb create mode 100644 activesupport/test/cache/behaviors/cache_delete_matched_behavior.rb create mode 100644 activesupport/test/cache/behaviors/cache_increment_decrement_behavior.rb create mode 100644 activesupport/test/cache/behaviors/cache_store_behavior.rb create mode 100644 activesupport/test/cache/behaviors/cache_store_version_behavior.rb create mode 100644 activesupport/test/cache/behaviors/encoded_key_cache_behavior.rb create mode 100644 activesupport/test/cache/behaviors/local_cache_behavior.rb create mode 100644 activesupport/test/cache/cache_entry_test.rb create mode 100644 activesupport/test/cache/cache_key_test.rb create mode 100644 activesupport/test/cache/cache_store_logger_test.rb create mode 100644 activesupport/test/cache/cache_store_namespace_test.rb create mode 100644 activesupport/test/cache/cache_store_setting_test.rb create mode 100644 activesupport/test/cache/cache_store_write_multi_test.rb create mode 100644 activesupport/test/cache/local_cache_middleware_test.rb create mode 100644 activesupport/test/cache/stores/file_store_test.rb create mode 100644 activesupport/test/cache/stores/mem_cache_store_test.rb create mode 100644 activesupport/test/cache/stores/memory_store_test.rb create mode 100644 activesupport/test/cache/stores/null_store_test.rb delete mode 100644 activesupport/test/caching_test.rb (limited to 'activesupport') diff --git a/activesupport/test/cache/behaviors.rb b/activesupport/test/cache/behaviors.rb new file mode 100644 index 0000000000..efd045ac5e --- /dev/null +++ b/activesupport/test/cache/behaviors.rb @@ -0,0 +1,7 @@ +require_relative "behaviors/autoloading_cache_behavior" +require_relative "behaviors/cache_delete_matched_behavior" +require_relative "behaviors/cache_increment_decrement_behavior" +require_relative "behaviors/cache_store_behavior" +require_relative "behaviors/cache_store_version_behavior" +require_relative "behaviors/encoded_key_cache_behavior" +require_relative "behaviors/local_cache_behavior" diff --git a/activesupport/test/cache/behaviors/autoloading_cache_behavior.rb b/activesupport/test/cache/behaviors/autoloading_cache_behavior.rb new file mode 100644 index 0000000000..5f8af331f6 --- /dev/null +++ b/activesupport/test/cache/behaviors/autoloading_cache_behavior.rb @@ -0,0 +1,41 @@ +require "dependencies_test_helpers" + +module AutoloadingCacheBehavior + include DependenciesTestHelpers + + def test_simple_autoloading + with_autoloading_fixtures do + @cache.write("foo", EM.new) + end + + remove_constants(:EM) + ActiveSupport::Dependencies.clear + + with_autoloading_fixtures do + assert_kind_of EM, @cache.read("foo") + end + + remove_constants(:EM) + ActiveSupport::Dependencies.clear + end + + def test_two_classes_autoloading + with_autoloading_fixtures do + @cache.write("foo", [EM.new, ClassFolder.new]) + end + + remove_constants(:EM, :ClassFolder) + ActiveSupport::Dependencies.clear + + with_autoloading_fixtures do + loaded = @cache.read("foo") + assert_kind_of Array, loaded + assert_equal 2, loaded.size + assert_kind_of EM, loaded[0] + assert_kind_of ClassFolder, loaded[1] + end + + remove_constants(:EM, :ClassFolder) + ActiveSupport::Dependencies.clear + end +end diff --git a/activesupport/test/cache/behaviors/cache_delete_matched_behavior.rb b/activesupport/test/cache/behaviors/cache_delete_matched_behavior.rb new file mode 100644 index 0000000000..b872eb0279 --- /dev/null +++ b/activesupport/test/cache/behaviors/cache_delete_matched_behavior.rb @@ -0,0 +1,13 @@ +module CacheDeleteMatchedBehavior + def test_delete_matched + @cache.write("foo", "bar") + @cache.write("fu", "baz") + @cache.write("foo/bar", "baz") + @cache.write("fu/baz", "bar") + @cache.delete_matched(/oo/) + assert !@cache.exist?("foo") + assert @cache.exist?("fu") + assert !@cache.exist?("foo/bar") + assert @cache.exist?("fu/baz") + end +end diff --git a/activesupport/test/cache/behaviors/cache_increment_decrement_behavior.rb b/activesupport/test/cache/behaviors/cache_increment_decrement_behavior.rb new file mode 100644 index 0000000000..0d32339565 --- /dev/null +++ b/activesupport/test/cache/behaviors/cache_increment_decrement_behavior.rb @@ -0,0 +1,21 @@ +module CacheIncrementDecrementBehavior + def test_increment + @cache.write("foo", 1, raw: true) + assert_equal 1, @cache.read("foo").to_i + assert_equal 2, @cache.increment("foo") + assert_equal 2, @cache.read("foo").to_i + assert_equal 3, @cache.increment("foo") + assert_equal 3, @cache.read("foo").to_i + assert_nil @cache.increment("bar") + end + + def test_decrement + @cache.write("foo", 3, raw: true) + assert_equal 3, @cache.read("foo").to_i + assert_equal 2, @cache.decrement("foo") + assert_equal 2, @cache.read("foo").to_i + assert_equal 1, @cache.decrement("foo") + assert_equal 1, @cache.read("foo").to_i + assert_nil @cache.decrement("bar") + end +end diff --git a/activesupport/test/cache/behaviors/cache_store_behavior.rb b/activesupport/test/cache/behaviors/cache_store_behavior.rb new file mode 100644 index 0000000000..03c366e164 --- /dev/null +++ b/activesupport/test/cache/behaviors/cache_store_behavior.rb @@ -0,0 +1,329 @@ +# Tests the base functionality that should be identical across all cache stores. +module CacheStoreBehavior + def test_should_read_and_write_strings + assert @cache.write("foo", "bar") + assert_equal "bar", @cache.read("foo") + end + + def test_should_overwrite + @cache.write("foo", "bar") + @cache.write("foo", "baz") + assert_equal "baz", @cache.read("foo") + end + + def test_fetch_without_cache_miss + @cache.write("foo", "bar") + assert_not_called(@cache, :write) do + assert_equal "bar", @cache.fetch("foo") { "baz" } + end + end + + def test_fetch_with_cache_miss + assert_called_with(@cache, :write, ["foo", "baz", @cache.options]) do + assert_equal "baz", @cache.fetch("foo") { "baz" } + end + end + + def test_fetch_with_cache_miss_passes_key_to_block + cache_miss = false + assert_equal 3, @cache.fetch("foo") { |key| cache_miss = true; key.length } + assert cache_miss + + cache_miss = false + assert_equal 3, @cache.fetch("foo") { |key| cache_miss = true; key.length } + assert !cache_miss + end + + def test_fetch_with_forced_cache_miss + @cache.write("foo", "bar") + assert_not_called(@cache, :read) do + assert_called_with(@cache, :write, ["foo", "bar", @cache.options.merge(force: true)]) do + @cache.fetch("foo", force: true) { "bar" } + end + end + end + + def test_fetch_with_cached_nil + @cache.write("foo", nil) + assert_not_called(@cache, :write) do + assert_nil @cache.fetch("foo") { "baz" } + end + end + + def test_fetch_with_forced_cache_miss_with_block + @cache.write("foo", "bar") + assert_equal "foo_bar", @cache.fetch("foo", force: true) { "foo_bar" } + end + + def test_fetch_with_forced_cache_miss_without_block + @cache.write("foo", "bar") + assert_raises(ArgumentError) do + @cache.fetch("foo", force: true) + end + + assert_equal "bar", @cache.read("foo") + end + + def test_should_read_and_write_hash + assert @cache.write("foo", a: "b") + assert_equal({ a: "b" }, @cache.read("foo")) + end + + def test_should_read_and_write_integer + assert @cache.write("foo", 1) + assert_equal 1, @cache.read("foo") + end + + def test_should_read_and_write_nil + assert @cache.write("foo", nil) + assert_nil @cache.read("foo") + end + + def test_should_read_and_write_false + assert @cache.write("foo", false) + assert_equal false, @cache.read("foo") + end + + def test_read_multi + @cache.write("foo", "bar") + @cache.write("fu", "baz") + @cache.write("fud", "biz") + assert_equal({ "foo" => "bar", "fu" => "baz" }, @cache.read_multi("foo", "fu")) + end + + def test_read_multi_with_expires + time = Time.now + @cache.write("foo", "bar", expires_in: 10) + @cache.write("fu", "baz") + @cache.write("fud", "biz") + Time.stub(:now, time + 11) do + assert_equal({ "fu" => "baz" }, @cache.read_multi("foo", "fu")) + end + end + + def test_fetch_multi + @cache.write("foo", "bar") + @cache.write("fud", "biz") + + values = @cache.fetch_multi("foo", "fu", "fud") { |value| value * 2 } + + assert_equal({ "foo" => "bar", "fu" => "fufu", "fud" => "biz" }, values) + assert_equal("fufu", @cache.read("fu")) + end + + def test_multi_with_objects + cache_struct = Struct.new(:cache_key, :title) + foo = cache_struct.new("foo", "FOO!") + bar = cache_struct.new("bar") + + @cache.write("bar", "BAM!") + + values = @cache.fetch_multi(foo, bar) { |object| object.title } + + assert_equal({ foo => "FOO!", bar => "BAM!" }, values) + end + + def test_fetch_multi_without_block + assert_raises(ArgumentError) do + @cache.fetch_multi("foo") + end + end + + def test_read_and_write_compressed_small_data + @cache.write("foo", "bar", compress: true) + assert_equal "bar", @cache.read("foo") + end + + def test_read_and_write_compressed_large_data + @cache.write("foo", "bar", compress: true, compress_threshold: 2) + assert_equal "bar", @cache.read("foo") + end + + def test_read_and_write_compressed_nil + @cache.write("foo", nil, compress: true) + assert_nil @cache.read("foo") + end + + def test_cache_key + obj = Object.new + def obj.cache_key + :foo + end + @cache.write(obj, "bar") + assert_equal "bar", @cache.read("foo") + end + + def test_param_as_cache_key + obj = Object.new + def obj.to_param + "foo" + end + @cache.write(obj, "bar") + assert_equal "bar", @cache.read("foo") + end + + def test_array_as_cache_key + @cache.write([:fu, "foo"], "bar") + assert_equal "bar", @cache.read("fu/foo") + end + + def test_hash_as_cache_key + @cache.write({ foo: 1, fu: 2 }, "bar") + assert_equal "bar", @cache.read("foo=1/fu=2") + end + + def test_keys_are_case_sensitive + @cache.write("foo", "bar") + assert_nil @cache.read("FOO") + end + + def test_exist + @cache.write("foo", "bar") + assert_equal true, @cache.exist?("foo") + assert_equal false, @cache.exist?("bar") + end + + def test_nil_exist + @cache.write("foo", nil) + assert @cache.exist?("foo") + end + + def test_delete + @cache.write("foo", "bar") + assert @cache.exist?("foo") + assert @cache.delete("foo") + assert !@cache.exist?("foo") + end + + def test_original_store_objects_should_not_be_immutable + bar = "bar" + @cache.write("foo", bar) + assert_nothing_raised { bar.gsub!(/.*/, "baz") } + end + + def test_expires_in + time = Time.local(2008, 4, 24) + + Time.stub(:now, time) do + @cache.write("foo", "bar") + assert_equal "bar", @cache.read("foo") + end + + Time.stub(:now, time + 30) do + assert_equal "bar", @cache.read("foo") + end + + Time.stub(:now, time + 61) do + assert_nil @cache.read("foo") + end + end + + def test_race_condition_protection_skipped_if_not_defined + @cache.write("foo", "bar") + time = @cache.send(:read_entry, @cache.send(:normalize_key, "foo", {}), {}).expires_at + + Time.stub(:now, Time.at(time)) do + result = @cache.fetch("foo") do + assert_nil @cache.read("foo") + "baz" + end + assert_equal "baz", result + end + end + + def test_race_condition_protection_is_limited + time = Time.now + @cache.write("foo", "bar", expires_in: 60) + Time.stub(:now, time + 71) do + result = @cache.fetch("foo", race_condition_ttl: 10) do + assert_nil @cache.read("foo") + "baz" + end + assert_equal "baz", result + end + end + + def test_race_condition_protection_is_safe + time = Time.now + @cache.write("foo", "bar", expires_in: 60) + Time.stub(:now, time + 61) do + begin + @cache.fetch("foo", race_condition_ttl: 10) do + assert_equal "bar", @cache.read("foo") + raise ArgumentError.new + end + rescue ArgumentError + end + assert_equal "bar", @cache.read("foo") + end + Time.stub(:now, time + 91) do + assert_nil @cache.read("foo") + end + end + + def test_race_condition_protection + time = Time.now + @cache.write("foo", "bar", expires_in: 60) + Time.stub(:now, time + 61) do + result = @cache.fetch("foo", race_condition_ttl: 10) do + assert_equal "bar", @cache.read("foo") + "baz" + end + assert_equal "baz", result + end + end + + def test_crazy_key_characters + crazy_key = "#/:*(<+=> )&$%@?;'\"\'`~-" + assert @cache.write(crazy_key, "1", raw: true) + assert_equal "1", @cache.read(crazy_key) + assert_equal "1", @cache.fetch(crazy_key) + assert @cache.delete(crazy_key) + assert_equal "2", @cache.fetch(crazy_key, raw: true) { "2" } + assert_equal 3, @cache.increment(crazy_key) + assert_equal 2, @cache.decrement(crazy_key) + end + + def test_really_long_keys + key = "" + 900.times { key << "x" } + assert @cache.write(key, "bar") + assert_equal "bar", @cache.read(key) + assert_equal "bar", @cache.fetch(key) + assert_nil @cache.read("#{key}x") + assert_equal({ key => "bar" }, @cache.read_multi(key)) + assert @cache.delete(key) + end + + def test_cache_hit_instrumentation + key = "test_key" + @events = [] + ActiveSupport::Notifications.subscribe "cache_read.active_support" do |*args| + @events << ActiveSupport::Notifications::Event.new(*args) + end + assert @cache.write(key, "1", raw: true) + assert @cache.fetch(key) {} + assert_equal 1, @events.length + assert_equal "cache_read.active_support", @events[0].name + assert_equal :fetch, @events[0].payload[:super_operation] + assert @events[0].payload[:hit] + ensure + ActiveSupport::Notifications.unsubscribe "cache_read.active_support" + end + + def test_cache_miss_instrumentation + @events = [] + ActiveSupport::Notifications.subscribe(/^cache_(.*)\.active_support$/) do |*args| + @events << ActiveSupport::Notifications::Event.new(*args) + end + assert_not @cache.fetch("bad_key") {} + assert_equal 3, @events.length + assert_equal "cache_read.active_support", @events[0].name + assert_equal "cache_generate.active_support", @events[1].name + assert_equal "cache_write.active_support", @events[2].name + assert_equal :fetch, @events[0].payload[:super_operation] + assert_not @events[0].payload[:hit] + ensure + ActiveSupport::Notifications.unsubscribe "cache_read.active_support" + end +end diff --git a/activesupport/test/cache/behaviors/cache_store_version_behavior.rb b/activesupport/test/cache/behaviors/cache_store_version_behavior.rb new file mode 100644 index 0000000000..a0170c896f --- /dev/null +++ b/activesupport/test/cache/behaviors/cache_store_version_behavior.rb @@ -0,0 +1,86 @@ +module CacheStoreVersionBehavior + ModelWithKeyAndVersion = Struct.new(:cache_key, :cache_version) + + def test_fetch_with_right_version_should_hit + @cache.fetch("foo", version: 1) { "bar" } + assert_equal "bar", @cache.read("foo", version: 1) + end + + def test_fetch_with_wrong_version_should_miss + @cache.fetch("foo", version: 1) { "bar" } + assert_nil @cache.read("foo", version: 2) + end + + def test_read_with_right_version_should_hit + @cache.write("foo", "bar", version: 1) + assert_equal "bar", @cache.read("foo", version: 1) + end + + def test_read_with_wrong_version_should_miss + @cache.write("foo", "bar", version: 1) + assert_nil @cache.read("foo", version: 2) + end + + def test_exist_with_right_version_should_be_true + @cache.write("foo", "bar", version: 1) + assert @cache.exist?("foo", version: 1) + end + + def test_exist_with_wrong_version_should_be_false + @cache.write("foo", "bar", version: 1) + assert !@cache.exist?("foo", version: 2) + end + + def test_reading_and_writing_with_model_supporting_cache_version + m1v1 = ModelWithKeyAndVersion.new("model/1", 1) + m1v2 = ModelWithKeyAndVersion.new("model/1", 2) + + @cache.write(m1v1, "bar") + assert_equal "bar", @cache.read(m1v1) + assert_nil @cache.read(m1v2) + end + + def test_reading_and_writing_with_model_supporting_cache_version_using_nested_key + m1v1 = ModelWithKeyAndVersion.new("model/1", 1) + m1v2 = ModelWithKeyAndVersion.new("model/1", 2) + + @cache.write([ "something", m1v1 ], "bar") + assert_equal "bar", @cache.read([ "something", m1v1 ]) + assert_nil @cache.read([ "something", m1v2 ]) + end + + def test_fetching_with_model_supporting_cache_version + m1v1 = ModelWithKeyAndVersion.new("model/1", 1) + m1v2 = ModelWithKeyAndVersion.new("model/1", 2) + + @cache.fetch(m1v1) { "bar" } + assert_equal "bar", @cache.fetch(m1v1) { "bu" } + assert_equal "bu", @cache.fetch(m1v2) { "bu" } + end + + def test_exist_with_model_supporting_cache_version + m1v1 = ModelWithKeyAndVersion.new("model/1", 1) + m1v2 = ModelWithKeyAndVersion.new("model/1", 2) + + @cache.write(m1v1, "bar") + assert @cache.exist?(m1v1) + assert_not @cache.fetch(m1v2) + end + + def test_fetch_multi_with_model_supporting_cache_version + m1v1 = ModelWithKeyAndVersion.new("model/1", 1) + m2v1 = ModelWithKeyAndVersion.new("model/2", 1) + m2v2 = ModelWithKeyAndVersion.new("model/2", 2) + + first_fetch_values = @cache.fetch_multi(m1v1, m2v1) { |m| m.cache_key } + second_fetch_values = @cache.fetch_multi(m1v1, m2v2) { |m| m.cache_key + " 2nd" } + + assert_equal({ m1v1 => "model/1", m2v1 => "model/2" }, first_fetch_values) + assert_equal({ m1v1 => "model/1", m2v2 => "model/2 2nd" }, second_fetch_values) + end + + def test_version_is_normalized + @cache.write("foo", "bar", version: 1) + assert_equal "bar", @cache.read("foo", version: "1") + end +end diff --git a/activesupport/test/cache/behaviors/encoded_key_cache_behavior.rb b/activesupport/test/cache/behaviors/encoded_key_cache_behavior.rb new file mode 100644 index 0000000000..4d8e2946b2 --- /dev/null +++ b/activesupport/test/cache/behaviors/encoded_key_cache_behavior.rb @@ -0,0 +1,34 @@ +# https://rails.lighthouseapp.com/projects/8994/tickets/6225-memcachestore-cant-deal-with-umlauts-and-special-characters +# The error is caused by character encodings that can't be compared with ASCII-8BIT regular expressions and by special +# characters like the umlaut in UTF-8. +module EncodedKeyCacheBehavior + Encoding.list.each do |encoding| + define_method "test_#{encoding.name.underscore}_encoded_values" do + key = "foo".force_encoding(encoding) + assert @cache.write(key, "1", raw: true) + assert_equal "1", @cache.read(key) + assert_equal "1", @cache.fetch(key) + assert @cache.delete(key) + assert_equal "2", @cache.fetch(key, raw: true) { "2" } + assert_equal 3, @cache.increment(key) + assert_equal 2, @cache.decrement(key) + end + end + + def test_common_utf8_values + key = "\xC3\xBCmlaut".force_encoding(Encoding::UTF_8) + assert @cache.write(key, "1", raw: true) + assert_equal "1", @cache.read(key) + assert_equal "1", @cache.fetch(key) + assert @cache.delete(key) + assert_equal "2", @cache.fetch(key, raw: true) { "2" } + assert_equal 3, @cache.increment(key) + assert_equal 2, @cache.decrement(key) + end + + def test_retains_encoding + key = "\xC3\xBCmlaut".force_encoding(Encoding::UTF_8) + assert @cache.write(key, "1", raw: true) + assert_equal Encoding::UTF_8, key.encoding + end +end diff --git a/activesupport/test/cache/behaviors/local_cache_behavior.rb b/activesupport/test/cache/behaviors/local_cache_behavior.rb new file mode 100644 index 0000000000..3fb358bd0c --- /dev/null +++ b/activesupport/test/cache/behaviors/local_cache_behavior.rb @@ -0,0 +1,111 @@ +module LocalCacheBehavior + def test_local_writes_are_persistent_on_the_remote_cache + retval = @cache.with_local_cache do + @cache.write("foo", "bar") + end + assert retval + assert_equal "bar", @cache.read("foo") + end + + def test_clear_also_clears_local_cache + @cache.with_local_cache do + @cache.write("foo", "bar") + @cache.clear + assert_nil @cache.read("foo") + end + + assert_nil @cache.read("foo") + end + + def test_local_cache_of_write + @cache.with_local_cache do + @cache.write("foo", "bar") + @peek.delete("foo") + assert_equal "bar", @cache.read("foo") + end + end + + def test_local_cache_of_read + @cache.write("foo", "bar") + @cache.with_local_cache do + assert_equal "bar", @cache.read("foo") + end + end + + def test_local_cache_of_read_nil + @cache.with_local_cache do + assert_nil @cache.read("foo") + @cache.send(:bypass_local_cache) { @cache.write "foo", "bar" } + assert_nil @cache.read("foo") + end + end + + def test_local_cache_fetch + @cache.with_local_cache do + @cache.send(:local_cache).write "foo", "bar" + assert_equal "bar", @cache.send(:local_cache).fetch("foo") + end + end + + def test_local_cache_of_write_nil + @cache.with_local_cache do + assert @cache.write("foo", nil) + assert_nil @cache.read("foo") + @peek.write("foo", "bar") + assert_nil @cache.read("foo") + end + end + + def test_local_cache_of_write_with_unless_exist + @cache.with_local_cache do + @cache.write("foo", "bar") + @cache.write("foo", "baz", unless_exist: true) + assert_equal @peek.read("foo"), @cache.read("foo") + end + end + + def test_local_cache_of_delete + @cache.with_local_cache do + @cache.write("foo", "bar") + @cache.delete("foo") + assert_nil @cache.read("foo") + end + end + + def test_local_cache_of_exist + @cache.with_local_cache do + @cache.write("foo", "bar") + @peek.delete("foo") + assert @cache.exist?("foo") + end + end + + def test_local_cache_of_increment + @cache.with_local_cache do + @cache.write("foo", 1, raw: true) + @peek.write("foo", 2, raw: true) + @cache.increment("foo") + assert_equal 3, @cache.read("foo") + end + end + + def test_local_cache_of_decrement + @cache.with_local_cache do + @cache.write("foo", 1, raw: true) + @peek.write("foo", 3, raw: true) + @cache.decrement("foo") + assert_equal 2, @cache.read("foo") + end + end + + def test_middleware + app = lambda { |env| + result = @cache.write("foo", "bar") + assert_equal "bar", @cache.read("foo") # make sure 'foo' was written + assert result + [200, {}, []] + } + app = @cache.middleware.new(app) + app.call({}) + end +end diff --git a/activesupport/test/cache/cache_entry_test.rb b/activesupport/test/cache/cache_entry_test.rb new file mode 100644 index 0000000000..e446e39b10 --- /dev/null +++ b/activesupport/test/cache/cache_entry_test.rb @@ -0,0 +1,28 @@ +require "abstract_unit" +require "active_support/cache" + +class CacheEntryTest < ActiveSupport::TestCase + def test_expired + entry = ActiveSupport::Cache::Entry.new("value") + assert !entry.expired?, "entry not expired" + entry = ActiveSupport::Cache::Entry.new("value", expires_in: 60) + assert !entry.expired?, "entry not expired" + Time.stub(:now, Time.now + 61) do + assert entry.expired?, "entry is expired" + end + end + + def test_compress_values + value = "value" * 100 + entry = ActiveSupport::Cache::Entry.new(value, compress: true, compress_threshold: 1) + assert_equal value, entry.value + assert(value.bytesize > entry.size, "value is compressed") + end + + def test_non_compress_values + value = "value" * 100 + entry = ActiveSupport::Cache::Entry.new(value) + assert_equal value, entry.value + assert_equal value.bytesize, entry.size + end +end diff --git a/activesupport/test/cache/cache_key_test.rb b/activesupport/test/cache/cache_key_test.rb new file mode 100644 index 0000000000..149d0f66ee --- /dev/null +++ b/activesupport/test/cache/cache_key_test.rb @@ -0,0 +1,88 @@ +require "abstract_unit" +require "active_support/cache" + +class CacheKeyTest < ActiveSupport::TestCase + def test_entry_legacy_optional_ivars + legacy = Class.new(ActiveSupport::Cache::Entry) do + def initialize(value, options = {}) + @value = value + @expires_in = nil + @created_at = nil + super + end + end + + entry = legacy.new "foo" + assert_equal "foo", entry.value + end + + def test_expand_cache_key + assert_equal "1/2/true", ActiveSupport::Cache.expand_cache_key([1, "2", true]) + assert_equal "name/1/2/true", ActiveSupport::Cache.expand_cache_key([1, "2", true], :name) + end + + def test_expand_cache_key_with_rails_cache_id + 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) + end + end + + def test_expand_cache_key_with_rails_app_version + with_env("RAILS_APP_VERSION" => "rails3") do + assert_equal "rails3/foo", ActiveSupport::Cache.expand_cache_key(:foo) + end + end + + def test_expand_cache_key_rails_cache_id_should_win_over_rails_app_version + with_env("RAILS_CACHE_ID" => "c99", "RAILS_APP_VERSION" => "rails3") do + assert_equal "c99/foo", ActiveSupport::Cache.expand_cache_key(:foo) + end + end + + def test_expand_cache_key_respond_to_cache_key + key = "foo" + def key.cache_key + :foo_key + end + assert_equal "foo_key", ActiveSupport::Cache.expand_cache_key(key) + end + + def test_expand_cache_key_array_with_something_that_responds_to_cache_key + key = "foo" + def key.cache_key + :foo_key + end + assert_equal "foo_key", ActiveSupport::Cache.expand_cache_key([key]) + end + + def test_expand_cache_key_of_nil + assert_equal "", ActiveSupport::Cache.expand_cache_key(nil) + end + + def test_expand_cache_key_of_false + assert_equal "false", ActiveSupport::Cache.expand_cache_key(false) + end + + def test_expand_cache_key_of_true + assert_equal "true", ActiveSupport::Cache.expand_cache_key(true) + end + + 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 diff --git a/activesupport/test/cache/cache_store_logger_test.rb b/activesupport/test/cache/cache_store_logger_test.rb new file mode 100644 index 0000000000..621cfebb10 --- /dev/null +++ b/activesupport/test/cache/cache_store_logger_test.rb @@ -0,0 +1,34 @@ +require "abstract_unit" +require "active_support/cache" + +class CacheStoreLoggerTest < ActiveSupport::TestCase + def setup + @cache = ActiveSupport::Cache.lookup_store(:memory_store) + + @buffer = StringIO.new + @cache.logger = ActiveSupport::Logger.new(@buffer) + end + + def test_logging + @cache.fetch("foo") { "bar" } + assert @buffer.string.present? + end + + def test_log_with_string_namespace + @cache.fetch("foo", namespace: "string_namespace") { "bar" } + assert_match %r{string_namespace:foo}, @buffer.string + end + + def test_log_with_proc_namespace + proc = Proc.new do + "proc_namespace" + end + @cache.fetch("foo", namespace: proc) { "bar" } + assert_match %r{proc_namespace:foo}, @buffer.string + end + + def test_mute_logging + @cache.mute { @cache.fetch("foo") { "bar" } } + assert @buffer.string.blank? + end +end diff --git a/activesupport/test/cache/cache_store_namespace_test.rb b/activesupport/test/cache/cache_store_namespace_test.rb new file mode 100644 index 0000000000..e395c88271 --- /dev/null +++ b/activesupport/test/cache/cache_store_namespace_test.rb @@ -0,0 +1,38 @@ +require "abstract_unit" +require "active_support/cache" + +class CacheStoreNamespaceTest < ActiveSupport::TestCase + def test_static_namespace + cache = ActiveSupport::Cache.lookup_store(:memory_store, namespace: "tester") + cache.write("foo", "bar") + assert_equal "bar", cache.read("foo") + assert_equal "bar", cache.instance_variable_get(:@data)["tester:foo"].value + end + + def test_proc_namespace + test_val = "tester" + proc = lambda { test_val } + cache = ActiveSupport::Cache.lookup_store(:memory_store, namespace: proc) + cache.write("foo", "bar") + assert_equal "bar", cache.read("foo") + assert_equal "bar", cache.instance_variable_get(:@data)["tester:foo"].value + end + + def test_delete_matched_key_start + cache = ActiveSupport::Cache.lookup_store(:memory_store, namespace: "tester") + cache.write("foo", "bar") + cache.write("fu", "baz") + cache.delete_matched(/^fo/) + assert !cache.exist?("foo") + assert cache.exist?("fu") + end + + def test_delete_matched_key + cache = ActiveSupport::Cache.lookup_store(:memory_store, namespace: "foo") + cache.write("foo", "bar") + cache.write("fu", "baz") + cache.delete_matched(/OO/i) + assert !cache.exist?("foo") + assert cache.exist?("fu") + end +end diff --git a/activesupport/test/cache/cache_store_setting_test.rb b/activesupport/test/cache/cache_store_setting_test.rb new file mode 100644 index 0000000000..cb9b006abe --- /dev/null +++ b/activesupport/test/cache/cache_store_setting_test.rb @@ -0,0 +1,66 @@ +require "abstract_unit" +require "active_support/cache" +require "dalli" + +class CacheStoreSettingTest < ActiveSupport::TestCase + def test_memory_store_gets_created_if_no_arguments_passed_to_lookup_store_method + store = ActiveSupport::Cache.lookup_store + assert_kind_of(ActiveSupport::Cache::MemoryStore, store) + end + + def test_memory_store + store = ActiveSupport::Cache.lookup_store :memory_store + assert_kind_of(ActiveSupport::Cache::MemoryStore, store) + end + + def test_file_fragment_cache_store + store = ActiveSupport::Cache.lookup_store :file_store, "/path/to/cache/directory" + assert_kind_of(ActiveSupport::Cache::FileStore, store) + assert_equal "/path/to/cache/directory", store.cache_path + end + + def test_mem_cache_fragment_cache_store + assert_called_with(Dalli::Client, :new, [%w[localhost], {}]) do + store = ActiveSupport::Cache.lookup_store :mem_cache_store, "localhost" + assert_kind_of(ActiveSupport::Cache::MemCacheStore, store) + end + end + + def test_mem_cache_fragment_cache_store_with_given_mem_cache + mem_cache = Dalli::Client.new + assert_not_called(Dalli::Client, :new) do + store = ActiveSupport::Cache.lookup_store :mem_cache_store, mem_cache + assert_kind_of(ActiveSupport::Cache::MemCacheStore, store) + end + end + + def test_mem_cache_fragment_cache_store_with_not_dalli_client + assert_not_called(Dalli::Client, :new) do + memcache = Object.new + assert_raises(ArgumentError) do + ActiveSupport::Cache.lookup_store :mem_cache_store, memcache + end + end + end + + def test_mem_cache_fragment_cache_store_with_multiple_servers + assert_called_with(Dalli::Client, :new, [%w[localhost 192.168.1.1], {}]) do + store = ActiveSupport::Cache.lookup_store :mem_cache_store, "localhost", "192.168.1.1" + assert_kind_of(ActiveSupport::Cache::MemCacheStore, store) + end + end + + def test_mem_cache_fragment_cache_store_with_options + assert_called_with(Dalli::Client, :new, [%w[localhost 192.168.1.1], { timeout: 10 }]) do + store = ActiveSupport::Cache.lookup_store :mem_cache_store, "localhost", "192.168.1.1", namespace: "foo", timeout: 10 + assert_kind_of(ActiveSupport::Cache::MemCacheStore, store) + assert_equal "foo", store.options[:namespace] + end + end + + def test_object_assigned_fragment_cache_store + store = ActiveSupport::Cache.lookup_store ActiveSupport::Cache::FileStore.new("/path/to/cache/directory") + assert_kind_of(ActiveSupport::Cache::FileStore, store) + assert_equal "/path/to/cache/directory", store.cache_path + end +end diff --git a/activesupport/test/cache/cache_store_write_multi_test.rb b/activesupport/test/cache/cache_store_write_multi_test.rb new file mode 100644 index 0000000000..16e3f3b842 --- /dev/null +++ b/activesupport/test/cache/cache_store_write_multi_test.rb @@ -0,0 +1,60 @@ +require "abstract_unit" +require "active_support/cache" + +class CacheStoreWriteMultiEntriesStoreProviderInterfaceTest < ActiveSupport::TestCase + setup do + @cache = ActiveSupport::Cache.lookup_store(:null_store) + end + + test "fetch_multi uses write_multi_entries store provider interface" do + assert_called_with(@cache, :write_multi_entries) do + @cache.fetch_multi "a", "b", "c" do |key| + key * 2 + end + end + end +end + +class CacheStoreWriteMultiInstrumentationTest < ActiveSupport::TestCase + setup do + @cache = ActiveSupport::Cache.lookup_store(:null_store) + end + + test "instrumentation" do + writes = { "a" => "aa", "b" => "bb" } + + events = with_instrumentation "write_multi" do + @cache.write_multi(writes) + end + + assert_equal %w[ cache_write_multi.active_support ], events.map(&:name) + assert_nil events[0].payload[:super_operation] + assert_equal({ "a" => "aa", "b" => "bb" }, events[0].payload[:key]) + end + + test "instrumentation with fetch_multi as super operation" do + skip "fetch_multi isn't instrumented yet" + + events = with_instrumentation "write_multi" do + @cache.fetch_multi("a", "b") { |key| key * 2 } + end + + assert_equal %w[ cache_write_multi.active_support ], events.map(&:name) + assert_nil events[0].payload[:super_operation] + assert !events[0].payload[:hit] + end + + private + def with_instrumentation(method) + event_name = "cache_#{method}.active_support" + + [].tap do |events| + ActiveSupport::Notifications.subscribe event_name do |*args| + events << ActiveSupport::Notifications::Event.new(*args) + end + yield + end + ensure + ActiveSupport::Notifications.unsubscribe event_name + end +end diff --git a/activesupport/test/cache/local_cache_middleware_test.rb b/activesupport/test/cache/local_cache_middleware_test.rb new file mode 100644 index 0000000000..352502fb43 --- /dev/null +++ b/activesupport/test/cache/local_cache_middleware_test.rb @@ -0,0 +1,61 @@ +require "abstract_unit" +require "active_support/cache" + +module ActiveSupport + module Cache + module Strategy + module LocalCache + class MiddlewareTest < ActiveSupport::TestCase + def test_local_cache_cleared_on_close + key = "super awesome key" + assert_nil LocalCacheRegistry.cache_for key + middleware = Middleware.new("<3", key).new(->(env) { + assert LocalCacheRegistry.cache_for(key), "should have a cache" + [200, {}, []] + }) + _, _, body = middleware.call({}) + assert LocalCacheRegistry.cache_for(key), "should still have a cache" + body.each {} + assert LocalCacheRegistry.cache_for(key), "should still have a cache" + body.close + assert_nil LocalCacheRegistry.cache_for(key) + end + + def test_local_cache_cleared_and_response_should_be_present_on_invalid_parameters_error + key = "super awesome key" + assert_nil LocalCacheRegistry.cache_for key + middleware = Middleware.new("<3", key).new(->(env) { + assert LocalCacheRegistry.cache_for(key), "should have a cache" + raise Rack::Utils::InvalidParameterError + }) + response = middleware.call({}) + assert response, "response should exist" + assert_nil LocalCacheRegistry.cache_for(key) + end + + def test_local_cache_cleared_on_exception + key = "super awesome key" + assert_nil LocalCacheRegistry.cache_for key + middleware = Middleware.new("<3", key).new(->(env) { + assert LocalCacheRegistry.cache_for(key), "should have a cache" + raise + }) + assert_raises(RuntimeError) { middleware.call({}) } + assert_nil LocalCacheRegistry.cache_for(key) + end + + def test_local_cache_cleared_on_throw + key = "super awesome key" + assert_nil LocalCacheRegistry.cache_for key + middleware = Middleware.new("<3", key).new(->(env) { + assert LocalCacheRegistry.cache_for(key), "should have a cache" + throw :warden + }) + assert_throws(:warden) { middleware.call({}) } + assert_nil LocalCacheRegistry.cache_for(key) + end + end + end + end + end +end diff --git a/activesupport/test/cache/stores/file_store_test.rb b/activesupport/test/cache/stores/file_store_test.rb new file mode 100644 index 0000000000..48b304fe6e --- /dev/null +++ b/activesupport/test/cache/stores/file_store_test.rb @@ -0,0 +1,128 @@ +require "abstract_unit" +require "active_support/cache" +require_relative "../behaviors" +require "pathname" + +class FileStoreTest < ActiveSupport::TestCase + def setup + Dir.mkdir(cache_dir) unless File.exist?(cache_dir) + @cache = ActiveSupport::Cache.lookup_store(:file_store, cache_dir, expires_in: 60) + @peek = ActiveSupport::Cache.lookup_store(:file_store, cache_dir, expires_in: 60) + @cache_with_pathname = ActiveSupport::Cache.lookup_store(:file_store, Pathname.new(cache_dir), expires_in: 60) + + @buffer = StringIO.new + @cache.logger = ActiveSupport::Logger.new(@buffer) + end + + def teardown + FileUtils.rm_r(cache_dir) + rescue Errno::ENOENT + end + + def cache_dir + File.join(Dir.pwd, "tmp_cache") + end + + include CacheStoreBehavior + include CacheStoreVersionBehavior + include LocalCacheBehavior + include CacheDeleteMatchedBehavior + include CacheIncrementDecrementBehavior + include AutoloadingCacheBehavior + + def test_clear + gitkeep = File.join(cache_dir, ".gitkeep") + keep = File.join(cache_dir, ".keep") + FileUtils.touch([gitkeep, keep]) + @cache.clear + assert File.exist?(gitkeep) + assert File.exist?(keep) + end + + def test_clear_without_cache_dir + FileUtils.rm_r(cache_dir) + @cache.clear + end + + def test_long_uri_encoded_keys + @cache.write("%" * 870, 1) + assert_equal 1, @cache.read("%" * 870) + end + + def test_key_transformation + key = @cache.send(:normalize_key, "views/index?id=1", {}) + assert_equal "views/index?id=1", @cache.send(:file_path_key, key) + end + + def test_key_transformation_with_pathname + FileUtils.touch(File.join(cache_dir, "foo")) + key = @cache_with_pathname.send(:normalize_key, "views/index?id=1", {}) + assert_equal "views/index?id=1", @cache_with_pathname.send(:file_path_key, key) + end + + # Test that generated cache keys are short enough to have Tempfile stuff added to them and + # remain valid + def test_filename_max_size + key = "#{'A' * ActiveSupport::Cache::FileStore::FILENAME_MAX_SIZE}" + path = @cache.send(:normalize_key, key, {}) + Dir::Tmpname.create(path) do |tmpname, n, opts| + assert File.basename(tmpname + ".lock").length <= 255, "Temp filename too long: #{File.basename(tmpname + '.lock').length}" + end + end + + # Because file systems have a maximum filename size, filenames > max size should be split in to directories + # If filename is 'AAAAB', where max size is 4, the returned path should be AAAA/B + def test_key_transformation_max_filename_size + key = "#{'A' * ActiveSupport::Cache::FileStore::FILENAME_MAX_SIZE}B" + path = @cache.send(:normalize_key, key, {}) + assert path.split("/").all? { |dir_name| dir_name.size <= ActiveSupport::Cache::FileStore::FILENAME_MAX_SIZE } + assert_equal "B", File.basename(path) + end + + # If nothing has been stored in the cache, there is a chance the cache directory does not yet exist + # Ensure delete_matched gracefully handles this case + def test_delete_matched_when_cache_directory_does_not_exist + assert_nothing_raised do + ActiveSupport::Cache::FileStore.new("/test/cache/directory").delete_matched(/does_not_exist/) + end + end + + def test_delete_does_not_delete_empty_parent_dir + sub_cache_dir = File.join(cache_dir, "subdir/") + sub_cache_store = ActiveSupport::Cache::FileStore.new(sub_cache_dir) + assert_nothing_raised do + assert sub_cache_store.write("foo", "bar") + assert sub_cache_store.delete("foo") + end + assert File.exist?(cache_dir), "Parent of top level cache dir was deleted!" + assert File.exist?(sub_cache_dir), "Top level cache dir was deleted!" + assert Dir.entries(sub_cache_dir).reject { |f| ActiveSupport::Cache::FileStore::EXCLUDED_DIRS.include?(f) }.empty? + end + + def test_log_exception_when_cache_read_fails + File.stub(:exist?, -> { raise StandardError.new("failed") }) do + @cache.send(:read_entry, "winston", {}) + assert @buffer.string.present? + end + end + + def test_cleanup_removes_all_expired_entries + time = Time.now + @cache.write("foo", "bar", expires_in: 10) + @cache.write("baz", "qux") + @cache.write("quux", "corge", expires_in: 20) + Time.stub(:now, time + 15) do + @cache.cleanup + assert_not @cache.exist?("foo") + assert @cache.exist?("baz") + assert @cache.exist?("quux") + end + end + + def test_write_with_unless_exist + assert_equal true, @cache.write(1, "aaaaaaaaaa") + assert_equal false, @cache.write(1, "aaaaaaaaaa", unless_exist: true) + @cache.write(1, nil) + assert_equal false, @cache.write(1, "aaaaaaaaaa", unless_exist: true) + end +end diff --git a/activesupport/test/cache/stores/mem_cache_store_test.rb b/activesupport/test/cache/stores/mem_cache_store_test.rb new file mode 100644 index 0000000000..2dd5264818 --- /dev/null +++ b/activesupport/test/cache/stores/mem_cache_store_test.rb @@ -0,0 +1,74 @@ +require "abstract_unit" +require "active_support/cache" +require_relative "../behaviors" +require "dalli" + +class MemCacheStoreTest < ActiveSupport::TestCase + begin + ss = Dalli::Client.new("localhost:11211").stats + raise Dalli::DalliError unless ss["localhost:11211"] + + MEMCACHE_UP = true + rescue Dalli::DalliError + $stderr.puts "Skipping memcached tests. Start memcached and try again." + MEMCACHE_UP = false + end + + def setup + skip "memcache server is not up" unless MEMCACHE_UP + + @cache = ActiveSupport::Cache.lookup_store(:mem_cache_store, expires_in: 60) + @peek = ActiveSupport::Cache.lookup_store(:mem_cache_store) + @data = @cache.instance_variable_get(:@data) + @cache.clear + @cache.silence! + @cache.logger = ActiveSupport::Logger.new("/dev/null") + end + + include CacheStoreBehavior + include CacheStoreVersionBehavior + include LocalCacheBehavior + include CacheIncrementDecrementBehavior + include EncodedKeyCacheBehavior + include AutoloadingCacheBehavior + + def test_raw_values + cache = ActiveSupport::Cache.lookup_store(:mem_cache_store, raw: true) + cache.clear + cache.write("foo", 2) + assert_equal "2", cache.read("foo") + end + + def test_raw_values_with_marshal + cache = ActiveSupport::Cache.lookup_store(:mem_cache_store, raw: true) + cache.clear + cache.write("foo", Marshal.dump([])) + assert_equal [], cache.read("foo") + end + + def test_local_cache_raw_values + cache = ActiveSupport::Cache.lookup_store(:mem_cache_store, raw: true) + cache.clear + cache.with_local_cache do + cache.write("foo", 2) + assert_equal "2", cache.read("foo") + end + end + + def test_local_cache_raw_values_with_marshal + cache = ActiveSupport::Cache.lookup_store(:mem_cache_store, raw: true) + cache.clear + cache.with_local_cache do + cache.write("foo", Marshal.dump([])) + assert_equal [], cache.read("foo") + end + end + + def test_read_should_return_a_different_object_id_each_time_it_is_called + @cache.write("foo", "bar") + 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 +end diff --git a/activesupport/test/cache/stores/memory_store_test.rb b/activesupport/test/cache/stores/memory_store_test.rb new file mode 100644 index 0000000000..3dd1646d56 --- /dev/null +++ b/activesupport/test/cache/stores/memory_store_test.rb @@ -0,0 +1,107 @@ +require "abstract_unit" +require "active_support/cache" +require_relative "../behaviors" + +class MemoryStoreTest < ActiveSupport::TestCase + def setup + @record_size = ActiveSupport::Cache.lookup_store(:memory_store).send(:cached_size, 1, ActiveSupport::Cache::Entry.new("aaaaaaaaaa")) + @cache = ActiveSupport::Cache.lookup_store(:memory_store, expires_in: 60, size: @record_size * 10 + 1) + end + + include CacheStoreBehavior + include CacheStoreVersionBehavior + include CacheDeleteMatchedBehavior + include CacheIncrementDecrementBehavior + + def test_prune_size + @cache.write(1, "aaaaaaaaaa") && sleep(0.001) + @cache.write(2, "bbbbbbbbbb") && sleep(0.001) + @cache.write(3, "cccccccccc") && sleep(0.001) + @cache.write(4, "dddddddddd") && sleep(0.001) + @cache.write(5, "eeeeeeeeee") && sleep(0.001) + @cache.read(2) && sleep(0.001) + @cache.read(4) + @cache.prune(@record_size * 3) + assert @cache.exist?(5) + assert @cache.exist?(4) + assert !@cache.exist?(3), "no entry" + assert @cache.exist?(2) + assert !@cache.exist?(1), "no entry" + end + + def test_prune_size_on_write + @cache.write(1, "aaaaaaaaaa") && sleep(0.001) + @cache.write(2, "bbbbbbbbbb") && sleep(0.001) + @cache.write(3, "cccccccccc") && sleep(0.001) + @cache.write(4, "dddddddddd") && sleep(0.001) + @cache.write(5, "eeeeeeeeee") && sleep(0.001) + @cache.write(6, "ffffffffff") && sleep(0.001) + @cache.write(7, "gggggggggg") && sleep(0.001) + @cache.write(8, "hhhhhhhhhh") && sleep(0.001) + @cache.write(9, "iiiiiiiiii") && sleep(0.001) + @cache.write(10, "kkkkkkkkkk") && sleep(0.001) + @cache.read(2) && sleep(0.001) + @cache.read(4) && sleep(0.001) + @cache.write(11, "llllllllll") + assert @cache.exist?(11) + assert @cache.exist?(10) + assert @cache.exist?(9) + assert @cache.exist?(8) + assert @cache.exist?(7) + assert !@cache.exist?(6), "no entry" + assert !@cache.exist?(5), "no entry" + assert @cache.exist?(4) + assert !@cache.exist?(3), "no entry" + assert @cache.exist?(2) + assert !@cache.exist?(1), "no entry" + end + + def test_prune_size_on_write_based_on_key_length + @cache.write(1, "aaaaaaaaaa") && sleep(0.001) + @cache.write(2, "bbbbbbbbbb") && sleep(0.001) + @cache.write(3, "cccccccccc") && sleep(0.001) + @cache.write(4, "dddddddddd") && sleep(0.001) + @cache.write(5, "eeeeeeeeee") && sleep(0.001) + @cache.write(6, "ffffffffff") && sleep(0.001) + @cache.write(7, "gggggggggg") && sleep(0.001) + @cache.write(8, "hhhhhhhhhh") && sleep(0.001) + @cache.write(9, "iiiiiiiiii") && sleep(0.001) + long_key = "*" * 2 * @record_size + @cache.write(long_key, "llllllllll") + assert @cache.exist?(long_key) + assert @cache.exist?(9) + assert @cache.exist?(8) + assert @cache.exist?(7) + assert @cache.exist?(6) + assert !@cache.exist?(5), "no entry" + assert !@cache.exist?(4), "no entry" + assert !@cache.exist?(3), "no entry" + assert !@cache.exist?(2), "no entry" + assert !@cache.exist?(1), "no entry" + end + + def test_pruning_is_capped_at_a_max_time + def @cache.delete_entry(*args) + sleep(0.01) + super + end + @cache.write(1, "aaaaaaaaaa") && sleep(0.001) + @cache.write(2, "bbbbbbbbbb") && sleep(0.001) + @cache.write(3, "cccccccccc") && sleep(0.001) + @cache.write(4, "dddddddddd") && sleep(0.001) + @cache.write(5, "eeeeeeeeee") && sleep(0.001) + @cache.prune(30, 0.001) + assert @cache.exist?(5) + assert @cache.exist?(4) + assert @cache.exist?(3) + assert @cache.exist?(2) + assert !@cache.exist?(1) + end + + def test_write_with_unless_exist + assert_equal true, @cache.write(1, "aaaaaaaaaa") + assert_equal false, @cache.write(1, "aaaaaaaaaa", unless_exist: true) + @cache.write(1, nil) + assert_equal false, @cache.write(1, "aaaaaaaaaa", unless_exist: true) + end +end diff --git a/activesupport/test/cache/stores/null_store_test.rb b/activesupport/test/cache/stores/null_store_test.rb new file mode 100644 index 0000000000..23c4e64ee4 --- /dev/null +++ b/activesupport/test/cache/stores/null_store_test.rb @@ -0,0 +1,57 @@ +require "abstract_unit" +require "active_support/cache" +require_relative "../behaviors" + +class NullStoreTest < ActiveSupport::TestCase + def setup + @cache = ActiveSupport::Cache.lookup_store(:null_store) + end + + def test_clear + @cache.clear + end + + def test_cleanup + @cache.cleanup + end + + def test_write + assert_equal true, @cache.write("name", "value") + end + + def test_read + @cache.write("name", "value") + assert_nil @cache.read("name") + end + + def test_delete + @cache.write("name", "value") + assert_equal false, @cache.delete("name") + end + + def test_increment + @cache.write("name", 1, raw: true) + assert_nil @cache.increment("name") + end + + def test_decrement + @cache.write("name", 1, raw: true) + assert_nil @cache.increment("name") + end + + def test_delete_matched + @cache.write("name", "value") + @cache.delete_matched(/name/) + end + + def test_local_store_strategy + @cache.with_local_cache do + @cache.write("name", "value") + assert_equal "value", @cache.read("name") + @cache.delete("name") + assert_nil @cache.read("name") + @cache.write("name", "value") + end + assert_nil @cache.read("name") + end +end diff --git a/activesupport/test/caching_test.rb b/activesupport/test/caching_test.rb deleted file mode 100644 index f2f8c58111..0000000000 --- a/activesupport/test/caching_test.rb +++ /dev/null @@ -1,1359 +0,0 @@ -require "logger" -require "abstract_unit" -require "active_support/cache" -require "dependencies_test_helpers" - -require "pathname" - -module ActiveSupport - module Cache - module Strategy - module LocalCache - class MiddlewareTest < ActiveSupport::TestCase - def test_local_cache_cleared_on_close - key = "super awesome key" - assert_nil LocalCacheRegistry.cache_for key - middleware = Middleware.new("<3", key).new(->(env) { - assert LocalCacheRegistry.cache_for(key), "should have a cache" - [200, {}, []] - }) - _, _, body = middleware.call({}) - assert LocalCacheRegistry.cache_for(key), "should still have a cache" - body.each {} - assert LocalCacheRegistry.cache_for(key), "should still have a cache" - body.close - assert_nil LocalCacheRegistry.cache_for(key) - end - - def test_local_cache_cleared_and_response_should_be_present_on_invalid_parameters_error - key = "super awesome key" - assert_nil LocalCacheRegistry.cache_for key - middleware = Middleware.new("<3", key).new(->(env) { - assert LocalCacheRegistry.cache_for(key), "should have a cache" - raise Rack::Utils::InvalidParameterError - }) - response = middleware.call({}) - assert response, "response should exist" - assert_nil LocalCacheRegistry.cache_for(key) - end - - def test_local_cache_cleared_on_exception - key = "super awesome key" - assert_nil LocalCacheRegistry.cache_for key - middleware = Middleware.new("<3", key).new(->(env) { - assert LocalCacheRegistry.cache_for(key), "should have a cache" - raise - }) - assert_raises(RuntimeError) { middleware.call({}) } - assert_nil LocalCacheRegistry.cache_for(key) - end - - def test_local_cache_cleared_on_throw - key = "super awesome key" - assert_nil LocalCacheRegistry.cache_for key - middleware = Middleware.new("<3", key).new(->(env) { - assert LocalCacheRegistry.cache_for(key), "should have a cache" - throw :warden - }) - assert_throws(:warden) { middleware.call({}) } - assert_nil LocalCacheRegistry.cache_for(key) - end - end - end - end - end -end - -class CacheKeyTest < ActiveSupport::TestCase - def test_entry_legacy_optional_ivars - legacy = Class.new(ActiveSupport::Cache::Entry) do - def initialize(value, options = {}) - @value = value - @expires_in = nil - @created_at = nil - super - end - end - - entry = legacy.new "foo" - assert_equal "foo", entry.value - end - - def test_expand_cache_key - assert_equal "1/2/true", ActiveSupport::Cache.expand_cache_key([1, "2", true]) - assert_equal "name/1/2/true", ActiveSupport::Cache.expand_cache_key([1, "2", true], :name) - end - - def test_expand_cache_key_with_rails_cache_id - 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) - end - end - - def test_expand_cache_key_with_rails_app_version - with_env("RAILS_APP_VERSION" => "rails3") do - assert_equal "rails3/foo", ActiveSupport::Cache.expand_cache_key(:foo) - end - end - - def test_expand_cache_key_rails_cache_id_should_win_over_rails_app_version - with_env("RAILS_CACHE_ID" => "c99", "RAILS_APP_VERSION" => "rails3") do - assert_equal "c99/foo", ActiveSupport::Cache.expand_cache_key(:foo) - end - end - - def test_expand_cache_key_respond_to_cache_key - key = "foo" - def key.cache_key - :foo_key - end - assert_equal "foo_key", ActiveSupport::Cache.expand_cache_key(key) - end - - def test_expand_cache_key_array_with_something_that_responds_to_cache_key - key = "foo" - def key.cache_key - :foo_key - end - assert_equal "foo_key", ActiveSupport::Cache.expand_cache_key([key]) - end - - def test_expand_cache_key_of_nil - assert_equal "", ActiveSupport::Cache.expand_cache_key(nil) - end - - def test_expand_cache_key_of_false - assert_equal "false", ActiveSupport::Cache.expand_cache_key(false) - end - - def test_expand_cache_key_of_true - assert_equal "true", ActiveSupport::Cache.expand_cache_key(true) - end - - 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 - def test_memory_store_gets_created_if_no_arguments_passed_to_lookup_store_method - store = ActiveSupport::Cache.lookup_store - assert_kind_of(ActiveSupport::Cache::MemoryStore, store) - end - - def test_memory_store - store = ActiveSupport::Cache.lookup_store :memory_store - assert_kind_of(ActiveSupport::Cache::MemoryStore, store) - end - - def test_file_fragment_cache_store - store = ActiveSupport::Cache.lookup_store :file_store, "/path/to/cache/directory" - assert_kind_of(ActiveSupport::Cache::FileStore, store) - assert_equal "/path/to/cache/directory", store.cache_path - end - - def test_mem_cache_fragment_cache_store - assert_called_with(Dalli::Client, :new, [%w[localhost], {}]) do - store = ActiveSupport::Cache.lookup_store :mem_cache_store, "localhost" - assert_kind_of(ActiveSupport::Cache::MemCacheStore, store) - end - end - - def test_mem_cache_fragment_cache_store_with_given_mem_cache - mem_cache = Dalli::Client.new - assert_not_called(Dalli::Client, :new) do - store = ActiveSupport::Cache.lookup_store :mem_cache_store, mem_cache - assert_kind_of(ActiveSupport::Cache::MemCacheStore, store) - end - end - - def test_mem_cache_fragment_cache_store_with_not_dalli_client - assert_not_called(Dalli::Client, :new) do - memcache = Object.new - assert_raises(ArgumentError) do - ActiveSupport::Cache.lookup_store :mem_cache_store, memcache - end - end - end - - def test_mem_cache_fragment_cache_store_with_multiple_servers - assert_called_with(Dalli::Client, :new, [%w[localhost 192.168.1.1], {}]) do - store = ActiveSupport::Cache.lookup_store :mem_cache_store, "localhost", "192.168.1.1" - assert_kind_of(ActiveSupport::Cache::MemCacheStore, store) - end - end - - def test_mem_cache_fragment_cache_store_with_options - assert_called_with(Dalli::Client, :new, [%w[localhost 192.168.1.1], { timeout: 10 }]) do - store = ActiveSupport::Cache.lookup_store :mem_cache_store, "localhost", "192.168.1.1", namespace: "foo", timeout: 10 - assert_kind_of(ActiveSupport::Cache::MemCacheStore, store) - assert_equal "foo", store.options[:namespace] - end - end - - def test_object_assigned_fragment_cache_store - store = ActiveSupport::Cache.lookup_store ActiveSupport::Cache::FileStore.new("/path/to/cache/directory") - assert_kind_of(ActiveSupport::Cache::FileStore, store) - assert_equal "/path/to/cache/directory", store.cache_path - end -end - -class CacheStoreNamespaceTest < ActiveSupport::TestCase - def test_static_namespace - cache = ActiveSupport::Cache.lookup_store(:memory_store, namespace: "tester") - cache.write("foo", "bar") - assert_equal "bar", cache.read("foo") - assert_equal "bar", cache.instance_variable_get(:@data)["tester:foo"].value - end - - def test_proc_namespace - test_val = "tester" - proc = lambda { test_val } - cache = ActiveSupport::Cache.lookup_store(:memory_store, namespace: proc) - cache.write("foo", "bar") - assert_equal "bar", cache.read("foo") - assert_equal "bar", cache.instance_variable_get(:@data)["tester:foo"].value - end - - def test_delete_matched_key_start - cache = ActiveSupport::Cache.lookup_store(:memory_store, namespace: "tester") - cache.write("foo", "bar") - cache.write("fu", "baz") - cache.delete_matched(/^fo/) - assert !cache.exist?("foo") - assert cache.exist?("fu") - end - - def test_delete_matched_key - cache = ActiveSupport::Cache.lookup_store(:memory_store, namespace: "foo") - cache.write("foo", "bar") - cache.write("fu", "baz") - cache.delete_matched(/OO/i) - assert !cache.exist?("foo") - assert cache.exist?("fu") - end -end - -# Tests the base functionality that should be identical across all cache stores. -module CacheStoreBehavior - def test_should_read_and_write_strings - assert @cache.write("foo", "bar") - assert_equal "bar", @cache.read("foo") - end - - def test_should_overwrite - @cache.write("foo", "bar") - @cache.write("foo", "baz") - assert_equal "baz", @cache.read("foo") - end - - def test_fetch_without_cache_miss - @cache.write("foo", "bar") - assert_not_called(@cache, :write) do - assert_equal "bar", @cache.fetch("foo") { "baz" } - end - end - - def test_fetch_with_cache_miss - assert_called_with(@cache, :write, ["foo", "baz", @cache.options]) do - assert_equal "baz", @cache.fetch("foo") { "baz" } - end - end - - def test_fetch_with_cache_miss_passes_key_to_block - cache_miss = false - assert_equal 3, @cache.fetch("foo") { |key| cache_miss = true; key.length } - assert cache_miss - - cache_miss = false - assert_equal 3, @cache.fetch("foo") { |key| cache_miss = true; key.length } - assert !cache_miss - end - - def test_fetch_with_forced_cache_miss - @cache.write("foo", "bar") - assert_not_called(@cache, :read) do - assert_called_with(@cache, :write, ["foo", "bar", @cache.options.merge(force: true)]) do - @cache.fetch("foo", force: true) { "bar" } - end - end - end - - def test_fetch_with_cached_nil - @cache.write("foo", nil) - assert_not_called(@cache, :write) do - assert_nil @cache.fetch("foo") { "baz" } - end - end - - def test_fetch_with_forced_cache_miss_with_block - @cache.write("foo", "bar") - assert_equal "foo_bar", @cache.fetch("foo", force: true) { "foo_bar" } - end - - def test_fetch_with_forced_cache_miss_without_block - @cache.write("foo", "bar") - assert_raises(ArgumentError) do - @cache.fetch("foo", force: true) - end - - assert_equal "bar", @cache.read("foo") - end - - def test_should_read_and_write_hash - assert @cache.write("foo", a: "b") - assert_equal({ a: "b" }, @cache.read("foo")) - end - - def test_should_read_and_write_integer - assert @cache.write("foo", 1) - assert_equal 1, @cache.read("foo") - end - - def test_should_read_and_write_nil - assert @cache.write("foo", nil) - assert_nil @cache.read("foo") - end - - def test_should_read_and_write_false - assert @cache.write("foo", false) - assert_equal false, @cache.read("foo") - end - - def test_read_multi - @cache.write("foo", "bar") - @cache.write("fu", "baz") - @cache.write("fud", "biz") - assert_equal({ "foo" => "bar", "fu" => "baz" }, @cache.read_multi("foo", "fu")) - end - - def test_read_multi_with_expires - time = Time.now - @cache.write("foo", "bar", expires_in: 10) - @cache.write("fu", "baz") - @cache.write("fud", "biz") - Time.stub(:now, time + 11) do - assert_equal({ "fu" => "baz" }, @cache.read_multi("foo", "fu")) - end - end - - def test_fetch_multi - @cache.write("foo", "bar") - @cache.write("fud", "biz") - - values = @cache.fetch_multi("foo", "fu", "fud") { |value| value * 2 } - - assert_equal({ "foo" => "bar", "fu" => "fufu", "fud" => "biz" }, values) - assert_equal("fufu", @cache.read("fu")) - end - - def test_multi_with_objects - cache_struct = Struct.new(:cache_key, :title) - foo = cache_struct.new("foo", "FOO!") - bar = cache_struct.new("bar") - - @cache.write("bar", "BAM!") - - values = @cache.fetch_multi(foo, bar) { |object| object.title } - - assert_equal({ foo => "FOO!", bar => "BAM!" }, values) - end - - def test_fetch_multi_without_block - assert_raises(ArgumentError) do - @cache.fetch_multi("foo") - end - end - - def test_read_and_write_compressed_small_data - @cache.write("foo", "bar", compress: true) - assert_equal "bar", @cache.read("foo") - end - - def test_read_and_write_compressed_large_data - @cache.write("foo", "bar", compress: true, compress_threshold: 2) - assert_equal "bar", @cache.read("foo") - end - - def test_read_and_write_compressed_nil - @cache.write("foo", nil, compress: true) - assert_nil @cache.read("foo") - end - - def test_cache_key - obj = Object.new - def obj.cache_key - :foo - end - @cache.write(obj, "bar") - assert_equal "bar", @cache.read("foo") - end - - def test_param_as_cache_key - obj = Object.new - def obj.to_param - "foo" - end - @cache.write(obj, "bar") - assert_equal "bar", @cache.read("foo") - end - - def test_array_as_cache_key - @cache.write([:fu, "foo"], "bar") - assert_equal "bar", @cache.read("fu/foo") - end - - def test_hash_as_cache_key - @cache.write({ foo: 1, fu: 2 }, "bar") - assert_equal "bar", @cache.read("foo=1/fu=2") - end - - def test_keys_are_case_sensitive - @cache.write("foo", "bar") - assert_nil @cache.read("FOO") - end - - def test_exist - @cache.write("foo", "bar") - assert_equal true, @cache.exist?("foo") - assert_equal false, @cache.exist?("bar") - end - - def test_nil_exist - @cache.write("foo", nil) - assert @cache.exist?("foo") - end - - def test_delete - @cache.write("foo", "bar") - assert @cache.exist?("foo") - assert @cache.delete("foo") - assert !@cache.exist?("foo") - end - - def test_original_store_objects_should_not_be_immutable - bar = "bar" - @cache.write("foo", bar) - assert_nothing_raised { bar.gsub!(/.*/, "baz") } - end - - def test_expires_in - time = Time.local(2008, 4, 24) - - Time.stub(:now, time) do - @cache.write("foo", "bar") - assert_equal "bar", @cache.read("foo") - end - - Time.stub(:now, time + 30) do - assert_equal "bar", @cache.read("foo") - end - - Time.stub(:now, time + 61) do - assert_nil @cache.read("foo") - end - end - - def test_race_condition_protection_skipped_if_not_defined - @cache.write("foo", "bar") - time = @cache.send(:read_entry, @cache.send(:normalize_key, "foo", {}), {}).expires_at - - Time.stub(:now, Time.at(time)) do - result = @cache.fetch("foo") do - assert_nil @cache.read("foo") - "baz" - end - assert_equal "baz", result - end - end - - def test_race_condition_protection_is_limited - time = Time.now - @cache.write("foo", "bar", expires_in: 60) - Time.stub(:now, time + 71) do - result = @cache.fetch("foo", race_condition_ttl: 10) do - assert_nil @cache.read("foo") - "baz" - end - assert_equal "baz", result - end - end - - def test_race_condition_protection_is_safe - time = Time.now - @cache.write("foo", "bar", expires_in: 60) - Time.stub(:now, time + 61) do - begin - @cache.fetch("foo", race_condition_ttl: 10) do - assert_equal "bar", @cache.read("foo") - raise ArgumentError.new - end - rescue ArgumentError - end - assert_equal "bar", @cache.read("foo") - end - Time.stub(:now, time + 91) do - assert_nil @cache.read("foo") - end - end - - def test_race_condition_protection - time = Time.now - @cache.write("foo", "bar", expires_in: 60) - Time.stub(:now, time + 61) do - result = @cache.fetch("foo", race_condition_ttl: 10) do - assert_equal "bar", @cache.read("foo") - "baz" - end - assert_equal "baz", result - end - end - - def test_crazy_key_characters - crazy_key = "#/:*(<+=> )&$%@?;'\"\'`~-" - assert @cache.write(crazy_key, "1", raw: true) - assert_equal "1", @cache.read(crazy_key) - assert_equal "1", @cache.fetch(crazy_key) - assert @cache.delete(crazy_key) - assert_equal "2", @cache.fetch(crazy_key, raw: true) { "2" } - assert_equal 3, @cache.increment(crazy_key) - assert_equal 2, @cache.decrement(crazy_key) - end - - def test_really_long_keys - key = "" - 900.times { key << "x" } - assert @cache.write(key, "bar") - assert_equal "bar", @cache.read(key) - assert_equal "bar", @cache.fetch(key) - assert_nil @cache.read("#{key}x") - assert_equal({ key => "bar" }, @cache.read_multi(key)) - assert @cache.delete(key) - end - - def test_cache_hit_instrumentation - key = "test_key" - @events = [] - ActiveSupport::Notifications.subscribe "cache_read.active_support" do |*args| - @events << ActiveSupport::Notifications::Event.new(*args) - end - assert @cache.write(key, "1", raw: true) - assert @cache.fetch(key) {} - assert_equal 1, @events.length - assert_equal "cache_read.active_support", @events[0].name - assert_equal :fetch, @events[0].payload[:super_operation] - assert @events[0].payload[:hit] - ensure - ActiveSupport::Notifications.unsubscribe "cache_read.active_support" - end - - def test_cache_miss_instrumentation - @events = [] - ActiveSupport::Notifications.subscribe(/^cache_(.*)\.active_support$/) do |*args| - @events << ActiveSupport::Notifications::Event.new(*args) - end - assert_not @cache.fetch("bad_key") {} - assert_equal 3, @events.length - assert_equal "cache_read.active_support", @events[0].name - assert_equal "cache_generate.active_support", @events[1].name - assert_equal "cache_write.active_support", @events[2].name - assert_equal :fetch, @events[0].payload[:super_operation] - assert_not @events[0].payload[:hit] - ensure - ActiveSupport::Notifications.unsubscribe "cache_read.active_support" - end -end - -module CacheStoreVersionBehavior - ModelWithKeyAndVersion = Struct.new(:cache_key, :cache_version) - - def test_fetch_with_right_version_should_hit - @cache.fetch("foo", version: 1) { "bar" } - assert_equal "bar", @cache.read("foo", version: 1) - end - - def test_fetch_with_wrong_version_should_miss - @cache.fetch("foo", version: 1) { "bar" } - assert_nil @cache.read("foo", version: 2) - end - - def test_read_with_right_version_should_hit - @cache.write("foo", "bar", version: 1) - assert_equal "bar", @cache.read("foo", version: 1) - end - - def test_read_with_wrong_version_should_miss - @cache.write("foo", "bar", version: 1) - assert_nil @cache.read("foo", version: 2) - end - - def test_exist_with_right_version_should_be_true - @cache.write("foo", "bar", version: 1) - assert @cache.exist?("foo", version: 1) - end - - def test_exist_with_wrong_version_should_be_false - @cache.write("foo", "bar", version: 1) - assert !@cache.exist?("foo", version: 2) - end - - def test_reading_and_writing_with_model_supporting_cache_version - m1v1 = ModelWithKeyAndVersion.new("model/1", 1) - m1v2 = ModelWithKeyAndVersion.new("model/1", 2) - - @cache.write(m1v1, "bar") - assert_equal "bar", @cache.read(m1v1) - assert_nil @cache.read(m1v2) - end - - def test_reading_and_writing_with_model_supporting_cache_version_using_nested_key - m1v1 = ModelWithKeyAndVersion.new("model/1", 1) - m1v2 = ModelWithKeyAndVersion.new("model/1", 2) - - @cache.write([ "something", m1v1 ], "bar") - assert_equal "bar", @cache.read([ "something", m1v1 ]) - assert_nil @cache.read([ "something", m1v2 ]) - end - - def test_fetching_with_model_supporting_cache_version - m1v1 = ModelWithKeyAndVersion.new("model/1", 1) - m1v2 = ModelWithKeyAndVersion.new("model/1", 2) - - @cache.fetch(m1v1) { "bar" } - assert_equal "bar", @cache.fetch(m1v1) { "bu" } - assert_equal "bu", @cache.fetch(m1v2) { "bu" } - end - - def test_exist_with_model_supporting_cache_version - m1v1 = ModelWithKeyAndVersion.new("model/1", 1) - m1v2 = ModelWithKeyAndVersion.new("model/1", 2) - - @cache.write(m1v1, "bar") - assert @cache.exist?(m1v1) - assert_not @cache.fetch(m1v2) - end - - def test_fetch_multi_with_model_supporting_cache_version - m1v1 = ModelWithKeyAndVersion.new("model/1", 1) - m2v1 = ModelWithKeyAndVersion.new("model/2", 1) - m2v2 = ModelWithKeyAndVersion.new("model/2", 2) - - first_fetch_values = @cache.fetch_multi(m1v1, m2v1) { |m| m.cache_key } - second_fetch_values = @cache.fetch_multi(m1v1, m2v2) { |m| m.cache_key + " 2nd" } - - assert_equal({ m1v1 => "model/1", m2v1 => "model/2" }, first_fetch_values) - assert_equal({ m1v1 => "model/1", m2v2 => "model/2 2nd" }, second_fetch_values) - end - - def test_version_is_normalized - @cache.write("foo", "bar", version: 1) - assert_equal "bar", @cache.read("foo", version: "1") - end -end - -# https://rails.lighthouseapp.com/projects/8994/tickets/6225-memcachestore-cant-deal-with-umlauts-and-special-characters -# The error is caused by character encodings that can't be compared with ASCII-8BIT regular expressions and by special -# characters like the umlaut in UTF-8. -module EncodedKeyCacheBehavior - Encoding.list.each do |encoding| - define_method "test_#{encoding.name.underscore}_encoded_values" do - key = "foo".force_encoding(encoding) - assert @cache.write(key, "1", raw: true) - assert_equal "1", @cache.read(key) - assert_equal "1", @cache.fetch(key) - assert @cache.delete(key) - assert_equal "2", @cache.fetch(key, raw: true) { "2" } - assert_equal 3, @cache.increment(key) - assert_equal 2, @cache.decrement(key) - end - end - - def test_common_utf8_values - key = "\xC3\xBCmlaut".force_encoding(Encoding::UTF_8) - assert @cache.write(key, "1", raw: true) - assert_equal "1", @cache.read(key) - assert_equal "1", @cache.fetch(key) - assert @cache.delete(key) - assert_equal "2", @cache.fetch(key, raw: true) { "2" } - assert_equal 3, @cache.increment(key) - assert_equal 2, @cache.decrement(key) - end - - def test_retains_encoding - key = "\xC3\xBCmlaut".force_encoding(Encoding::UTF_8) - assert @cache.write(key, "1", raw: true) - assert_equal Encoding::UTF_8, key.encoding - end -end - -module CacheDeleteMatchedBehavior - def test_delete_matched - @cache.write("foo", "bar") - @cache.write("fu", "baz") - @cache.write("foo/bar", "baz") - @cache.write("fu/baz", "bar") - @cache.delete_matched(/oo/) - assert !@cache.exist?("foo") - assert @cache.exist?("fu") - assert !@cache.exist?("foo/bar") - assert @cache.exist?("fu/baz") - end -end - -module CacheIncrementDecrementBehavior - def test_increment - @cache.write("foo", 1, raw: true) - assert_equal 1, @cache.read("foo").to_i - assert_equal 2, @cache.increment("foo") - assert_equal 2, @cache.read("foo").to_i - assert_equal 3, @cache.increment("foo") - assert_equal 3, @cache.read("foo").to_i - assert_nil @cache.increment("bar") - end - - def test_decrement - @cache.write("foo", 3, raw: true) - assert_equal 3, @cache.read("foo").to_i - assert_equal 2, @cache.decrement("foo") - assert_equal 2, @cache.read("foo").to_i - assert_equal 1, @cache.decrement("foo") - assert_equal 1, @cache.read("foo").to_i - assert_nil @cache.decrement("bar") - end -end - -module LocalCacheBehavior - def test_local_writes_are_persistent_on_the_remote_cache - retval = @cache.with_local_cache do - @cache.write("foo", "bar") - end - assert retval - assert_equal "bar", @cache.read("foo") - end - - def test_clear_also_clears_local_cache - @cache.with_local_cache do - @cache.write("foo", "bar") - @cache.clear - assert_nil @cache.read("foo") - end - - assert_nil @cache.read("foo") - end - - def test_local_cache_of_write - @cache.with_local_cache do - @cache.write("foo", "bar") - @peek.delete("foo") - assert_equal "bar", @cache.read("foo") - end - end - - def test_local_cache_of_read - @cache.write("foo", "bar") - @cache.with_local_cache do - assert_equal "bar", @cache.read("foo") - end - end - - def test_local_cache_of_read_nil - @cache.with_local_cache do - assert_nil @cache.read("foo") - @cache.send(:bypass_local_cache) { @cache.write "foo", "bar" } - assert_nil @cache.read("foo") - end - end - - def test_local_cache_fetch - @cache.with_local_cache do - @cache.send(:local_cache).write "foo", "bar" - assert_equal "bar", @cache.send(:local_cache).fetch("foo") - end - end - - def test_local_cache_of_write_nil - @cache.with_local_cache do - assert @cache.write("foo", nil) - assert_nil @cache.read("foo") - @peek.write("foo", "bar") - assert_nil @cache.read("foo") - end - end - - def test_local_cache_of_write_with_unless_exist - @cache.with_local_cache do - @cache.write("foo", "bar") - @cache.write("foo", "baz", unless_exist: true) - assert_equal @peek.read("foo"), @cache.read("foo") - end - end - - def test_local_cache_of_delete - @cache.with_local_cache do - @cache.write("foo", "bar") - @cache.delete("foo") - assert_nil @cache.read("foo") - end - end - - def test_local_cache_of_exist - @cache.with_local_cache do - @cache.write("foo", "bar") - @peek.delete("foo") - assert @cache.exist?("foo") - end - end - - def test_local_cache_of_increment - @cache.with_local_cache do - @cache.write("foo", 1, raw: true) - @peek.write("foo", 2, raw: true) - @cache.increment("foo") - assert_equal 3, @cache.read("foo") - end - end - - def test_local_cache_of_decrement - @cache.with_local_cache do - @cache.write("foo", 1, raw: true) - @peek.write("foo", 3, raw: true) - @cache.decrement("foo") - assert_equal 2, @cache.read("foo") - end - end - - def test_middleware - app = lambda { |env| - result = @cache.write("foo", "bar") - assert_equal "bar", @cache.read("foo") # make sure 'foo' was written - assert result - [200, {}, []] - } - app = @cache.middleware.new(app) - app.call({}) - end -end - -module AutoloadingCacheBehavior - include DependenciesTestHelpers - def test_simple_autoloading - with_autoloading_fixtures do - @cache.write("foo", EM.new) - end - - remove_constants(:EM) - ActiveSupport::Dependencies.clear - - with_autoloading_fixtures do - assert_kind_of EM, @cache.read("foo") - end - - remove_constants(:EM) - ActiveSupport::Dependencies.clear - end - - def test_two_classes_autoloading - with_autoloading_fixtures do - @cache.write("foo", [EM.new, ClassFolder.new]) - end - - remove_constants(:EM, :ClassFolder) - ActiveSupport::Dependencies.clear - - with_autoloading_fixtures do - loaded = @cache.read("foo") - assert_kind_of Array, loaded - assert_equal 2, loaded.size - assert_kind_of EM, loaded[0] - assert_kind_of ClassFolder, loaded[1] - end - - remove_constants(:EM, :ClassFolder) - ActiveSupport::Dependencies.clear - end -end - -class FileStoreTest < ActiveSupport::TestCase - def setup - Dir.mkdir(cache_dir) unless File.exist?(cache_dir) - @cache = ActiveSupport::Cache.lookup_store(:file_store, cache_dir, expires_in: 60) - @peek = ActiveSupport::Cache.lookup_store(:file_store, cache_dir, expires_in: 60) - @cache_with_pathname = ActiveSupport::Cache.lookup_store(:file_store, Pathname.new(cache_dir), expires_in: 60) - - @buffer = StringIO.new - @cache.logger = ActiveSupport::Logger.new(@buffer) - end - - def teardown - FileUtils.rm_r(cache_dir) - rescue Errno::ENOENT - end - - def cache_dir - File.join(Dir.pwd, "tmp_cache") - end - - include CacheStoreBehavior - include CacheStoreVersionBehavior - include LocalCacheBehavior - include CacheDeleteMatchedBehavior - include CacheIncrementDecrementBehavior - include AutoloadingCacheBehavior - - def test_clear - gitkeep = File.join(cache_dir, ".gitkeep") - keep = File.join(cache_dir, ".keep") - FileUtils.touch([gitkeep, keep]) - @cache.clear - assert File.exist?(gitkeep) - assert File.exist?(keep) - end - - def test_clear_without_cache_dir - FileUtils.rm_r(cache_dir) - @cache.clear - end - - def test_long_uri_encoded_keys - @cache.write("%" * 870, 1) - assert_equal 1, @cache.read("%" * 870) - end - - def test_key_transformation - key = @cache.send(:normalize_key, "views/index?id=1", {}) - assert_equal "views/index?id=1", @cache.send(:file_path_key, key) - end - - def test_key_transformation_with_pathname - FileUtils.touch(File.join(cache_dir, "foo")) - key = @cache_with_pathname.send(:normalize_key, "views/index?id=1", {}) - assert_equal "views/index?id=1", @cache_with_pathname.send(:file_path_key, key) - end - - # Test that generated cache keys are short enough to have Tempfile stuff added to them and - # remain valid - def test_filename_max_size - key = "#{'A' * ActiveSupport::Cache::FileStore::FILENAME_MAX_SIZE}" - path = @cache.send(:normalize_key, key, {}) - Dir::Tmpname.create(path) do |tmpname, n, opts| - assert File.basename(tmpname + ".lock").length <= 255, "Temp filename too long: #{File.basename(tmpname + '.lock').length}" - end - end - - # Because file systems have a maximum filename size, filenames > max size should be split in to directories - # If filename is 'AAAAB', where max size is 4, the returned path should be AAAA/B - def test_key_transformation_max_filename_size - key = "#{'A' * ActiveSupport::Cache::FileStore::FILENAME_MAX_SIZE}B" - path = @cache.send(:normalize_key, key, {}) - assert path.split("/").all? { |dir_name| dir_name.size <= ActiveSupport::Cache::FileStore::FILENAME_MAX_SIZE } - assert_equal "B", File.basename(path) - end - - # If nothing has been stored in the cache, there is a chance the cache directory does not yet exist - # Ensure delete_matched gracefully handles this case - def test_delete_matched_when_cache_directory_does_not_exist - assert_nothing_raised do - ActiveSupport::Cache::FileStore.new("/test/cache/directory").delete_matched(/does_not_exist/) - end - end - - def test_delete_does_not_delete_empty_parent_dir - sub_cache_dir = File.join(cache_dir, "subdir/") - sub_cache_store = ActiveSupport::Cache::FileStore.new(sub_cache_dir) - assert_nothing_raised do - assert sub_cache_store.write("foo", "bar") - assert sub_cache_store.delete("foo") - end - assert File.exist?(cache_dir), "Parent of top level cache dir was deleted!" - assert File.exist?(sub_cache_dir), "Top level cache dir was deleted!" - assert Dir.entries(sub_cache_dir).reject { |f| ActiveSupport::Cache::FileStore::EXCLUDED_DIRS.include?(f) }.empty? - end - - def test_log_exception_when_cache_read_fails - File.stub(:exist?, -> { raise StandardError.new("failed") }) do - @cache.send(:read_entry, "winston", {}) - assert @buffer.string.present? - end - end - - def test_cleanup_removes_all_expired_entries - time = Time.now - @cache.write("foo", "bar", expires_in: 10) - @cache.write("baz", "qux") - @cache.write("quux", "corge", expires_in: 20) - Time.stub(:now, time + 15) do - @cache.cleanup - assert_not @cache.exist?("foo") - assert @cache.exist?("baz") - assert @cache.exist?("quux") - end - end - - def test_write_with_unless_exist - assert_equal true, @cache.write(1, "aaaaaaaaaa") - assert_equal false, @cache.write(1, "aaaaaaaaaa", unless_exist: true) - @cache.write(1, nil) - assert_equal false, @cache.write(1, "aaaaaaaaaa", unless_exist: true) - end -end - -class MemoryStoreTest < ActiveSupport::TestCase - def setup - @record_size = ActiveSupport::Cache.lookup_store(:memory_store).send(:cached_size, 1, ActiveSupport::Cache::Entry.new("aaaaaaaaaa")) - @cache = ActiveSupport::Cache.lookup_store(:memory_store, expires_in: 60, size: @record_size * 10 + 1) - end - - include CacheStoreBehavior - include CacheStoreVersionBehavior - include CacheDeleteMatchedBehavior - include CacheIncrementDecrementBehavior - - def test_prune_size - @cache.write(1, "aaaaaaaaaa") && sleep(0.001) - @cache.write(2, "bbbbbbbbbb") && sleep(0.001) - @cache.write(3, "cccccccccc") && sleep(0.001) - @cache.write(4, "dddddddddd") && sleep(0.001) - @cache.write(5, "eeeeeeeeee") && sleep(0.001) - @cache.read(2) && sleep(0.001) - @cache.read(4) - @cache.prune(@record_size * 3) - assert @cache.exist?(5) - assert @cache.exist?(4) - assert !@cache.exist?(3), "no entry" - assert @cache.exist?(2) - assert !@cache.exist?(1), "no entry" - end - - def test_prune_size_on_write - @cache.write(1, "aaaaaaaaaa") && sleep(0.001) - @cache.write(2, "bbbbbbbbbb") && sleep(0.001) - @cache.write(3, "cccccccccc") && sleep(0.001) - @cache.write(4, "dddddddddd") && sleep(0.001) - @cache.write(5, "eeeeeeeeee") && sleep(0.001) - @cache.write(6, "ffffffffff") && sleep(0.001) - @cache.write(7, "gggggggggg") && sleep(0.001) - @cache.write(8, "hhhhhhhhhh") && sleep(0.001) - @cache.write(9, "iiiiiiiiii") && sleep(0.001) - @cache.write(10, "kkkkkkkkkk") && sleep(0.001) - @cache.read(2) && sleep(0.001) - @cache.read(4) && sleep(0.001) - @cache.write(11, "llllllllll") - assert @cache.exist?(11) - assert @cache.exist?(10) - assert @cache.exist?(9) - assert @cache.exist?(8) - assert @cache.exist?(7) - assert !@cache.exist?(6), "no entry" - assert !@cache.exist?(5), "no entry" - assert @cache.exist?(4) - assert !@cache.exist?(3), "no entry" - assert @cache.exist?(2) - assert !@cache.exist?(1), "no entry" - end - - def test_prune_size_on_write_based_on_key_length - @cache.write(1, "aaaaaaaaaa") && sleep(0.001) - @cache.write(2, "bbbbbbbbbb") && sleep(0.001) - @cache.write(3, "cccccccccc") && sleep(0.001) - @cache.write(4, "dddddddddd") && sleep(0.001) - @cache.write(5, "eeeeeeeeee") && sleep(0.001) - @cache.write(6, "ffffffffff") && sleep(0.001) - @cache.write(7, "gggggggggg") && sleep(0.001) - @cache.write(8, "hhhhhhhhhh") && sleep(0.001) - @cache.write(9, "iiiiiiiiii") && sleep(0.001) - long_key = "*" * 2 * @record_size - @cache.write(long_key, "llllllllll") - assert @cache.exist?(long_key) - assert @cache.exist?(9) - assert @cache.exist?(8) - assert @cache.exist?(7) - assert @cache.exist?(6) - assert !@cache.exist?(5), "no entry" - assert !@cache.exist?(4), "no entry" - assert !@cache.exist?(3), "no entry" - assert !@cache.exist?(2), "no entry" - assert !@cache.exist?(1), "no entry" - end - - def test_pruning_is_capped_at_a_max_time - def @cache.delete_entry(*args) - sleep(0.01) - super - end - @cache.write(1, "aaaaaaaaaa") && sleep(0.001) - @cache.write(2, "bbbbbbbbbb") && sleep(0.001) - @cache.write(3, "cccccccccc") && sleep(0.001) - @cache.write(4, "dddddddddd") && sleep(0.001) - @cache.write(5, "eeeeeeeeee") && sleep(0.001) - @cache.prune(30, 0.001) - assert @cache.exist?(5) - assert @cache.exist?(4) - assert @cache.exist?(3) - assert @cache.exist?(2) - assert !@cache.exist?(1) - end - - def test_write_with_unless_exist - assert_equal true, @cache.write(1, "aaaaaaaaaa") - assert_equal false, @cache.write(1, "aaaaaaaaaa", unless_exist: true) - @cache.write(1, nil) - assert_equal false, @cache.write(1, "aaaaaaaaaa", unless_exist: true) - end -end - -class MemCacheStoreTest < ActiveSupport::TestCase - require "dalli" - - begin - ss = Dalli::Client.new("localhost:11211").stats - raise Dalli::DalliError unless ss["localhost:11211"] - - MEMCACHE_UP = true - rescue Dalli::DalliError - $stderr.puts "Skipping memcached tests. Start memcached and try again." - MEMCACHE_UP = false - end - - def setup - skip "memcache server is not up" unless MEMCACHE_UP - - @cache = ActiveSupport::Cache.lookup_store(:mem_cache_store, expires_in: 60) - @peek = ActiveSupport::Cache.lookup_store(:mem_cache_store) - @data = @cache.instance_variable_get(:@data) - @cache.clear - @cache.silence! - @cache.logger = ActiveSupport::Logger.new("/dev/null") - end - - include CacheStoreBehavior - include CacheStoreVersionBehavior - include LocalCacheBehavior - include CacheIncrementDecrementBehavior - include EncodedKeyCacheBehavior - include AutoloadingCacheBehavior - - def test_raw_values - cache = ActiveSupport::Cache.lookup_store(:mem_cache_store, raw: true) - cache.clear - cache.write("foo", 2) - assert_equal "2", cache.read("foo") - end - - def test_raw_values_with_marshal - cache = ActiveSupport::Cache.lookup_store(:mem_cache_store, raw: true) - cache.clear - cache.write("foo", Marshal.dump([])) - assert_equal [], cache.read("foo") - end - - def test_local_cache_raw_values - cache = ActiveSupport::Cache.lookup_store(:mem_cache_store, raw: true) - cache.clear - cache.with_local_cache do - cache.write("foo", 2) - assert_equal "2", cache.read("foo") - end - end - - def test_local_cache_raw_values_with_marshal - cache = ActiveSupport::Cache.lookup_store(:mem_cache_store, raw: true) - cache.clear - cache.with_local_cache do - cache.write("foo", Marshal.dump([])) - assert_equal [], cache.read("foo") - end - end - - def test_read_should_return_a_different_object_id_each_time_it_is_called - @cache.write("foo", "bar") - 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 -end - -class NullStoreTest < ActiveSupport::TestCase - def setup - @cache = ActiveSupport::Cache.lookup_store(:null_store) - end - - def test_clear - @cache.clear - end - - def test_cleanup - @cache.cleanup - end - - def test_write - assert_equal true, @cache.write("name", "value") - end - - def test_read - @cache.write("name", "value") - assert_nil @cache.read("name") - end - - def test_delete - @cache.write("name", "value") - assert_equal false, @cache.delete("name") - end - - def test_increment - @cache.write("name", 1, raw: true) - assert_nil @cache.increment("name") - end - - def test_decrement - @cache.write("name", 1, raw: true) - assert_nil @cache.increment("name") - end - - def test_delete_matched - @cache.write("name", "value") - @cache.delete_matched(/name/) - end - - def test_local_store_strategy - @cache.with_local_cache do - @cache.write("name", "value") - assert_equal "value", @cache.read("name") - @cache.delete("name") - assert_nil @cache.read("name") - @cache.write("name", "value") - end - assert_nil @cache.read("name") - end -end - -class CacheStoreLoggerTest < ActiveSupport::TestCase - def setup - @cache = ActiveSupport::Cache.lookup_store(:memory_store) - - @buffer = StringIO.new - @cache.logger = ActiveSupport::Logger.new(@buffer) - end - - def test_logging - @cache.fetch("foo") { "bar" } - assert @buffer.string.present? - end - - def test_log_with_string_namespace - @cache.fetch("foo", namespace: "string_namespace") { "bar" } - assert_match %r{string_namespace:foo}, @buffer.string - end - - def test_log_with_proc_namespace - proc = Proc.new do - "proc_namespace" - end - @cache.fetch("foo", namespace: proc) { "bar" } - assert_match %r{proc_namespace:foo}, @buffer.string - end - - def test_mute_logging - @cache.mute { @cache.fetch("foo") { "bar" } } - assert @buffer.string.blank? - end -end - -class CacheEntryTest < ActiveSupport::TestCase - def test_expired - entry = ActiveSupport::Cache::Entry.new("value") - assert !entry.expired?, "entry not expired" - entry = ActiveSupport::Cache::Entry.new("value", expires_in: 60) - assert !entry.expired?, "entry not expired" - Time.stub(:now, Time.now + 61) do - assert entry.expired?, "entry is expired" - end - end - - def test_compress_values - value = "value" * 100 - entry = ActiveSupport::Cache::Entry.new(value, compress: true, compress_threshold: 1) - assert_equal value, entry.value - assert(value.bytesize > entry.size, "value is compressed") - end - - def test_non_compress_values - value = "value" * 100 - entry = ActiveSupport::Cache::Entry.new(value) - assert_equal value, entry.value - assert_equal value.bytesize, entry.size - end -end - -class CacheStoreWriteMultiEntriesStoreProviderInterfaceTest < ActiveSupport::TestCase - setup do - @cache = ActiveSupport::Cache.lookup_store(:null_store) - end - - test "fetch_multi uses write_multi_entries store provider interface" do - assert_called_with(@cache, :write_multi_entries) do - @cache.fetch_multi "a", "b", "c" do |key| - key * 2 - end - end - end -end - -class CacheStoreWriteMultiInstrumentationTest < ActiveSupport::TestCase - setup do - @cache = ActiveSupport::Cache.lookup_store(:null_store) - end - - test "instrumentation" do - writes = { "a" => "aa", "b" => "bb" } - - events = with_instrumentation "write_multi" do - @cache.write_multi(writes) - end - - assert_equal %w[ cache_write_multi.active_support ], events.map(&:name) - assert_nil events[0].payload[:super_operation] - assert_equal({ "a" => "aa", "b" => "bb" }, events[0].payload[:key]) - end - - test "instrumentation with fetch_multi as super operation" do - skip "fetch_multi isn't instrumented yet" - - events = with_instrumentation "write_multi" do - @cache.fetch_multi("a", "b") { |key| key * 2 } - end - - assert_equal %w[ cache_write_multi.active_support ], events.map(&:name) - assert_nil events[0].payload[:super_operation] - assert !events[0].payload[:hit] - end - - private - def with_instrumentation(method) - event_name = "cache_#{method}.active_support" - - [].tap do |events| - ActiveSupport::Notifications.subscribe event_name do |*args| - events << ActiveSupport::Notifications::Event.new(*args) - end - yield - end - ensure - ActiveSupport::Notifications.unsubscribe event_name - end -end -- cgit v1.2.3 From 816a3763d9bb070bf339de32c6a3ce31a45369f8 Mon Sep 17 00:00:00 2001 From: Jeremy Daer Date: Sat, 10 Jun 2017 11:01:43 -0700 Subject: Revert #25628. Incomplete change + needs a deprecation cycle. See https://github.com/rails/rails/issues/29067#issuecomment-301342084 for rationale. This reverts commit b76f82d714e590c20370e72fa36fa574c4f17650. Fixes #29067. Fixes #29081. --- activesupport/lib/active_support/cache.rb | 2 +- activesupport/lib/active_support/cache/file_store.rb | 2 +- activesupport/lib/active_support/cache/strategy/local_cache.rb | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) (limited to 'activesupport') diff --git a/activesupport/lib/active_support/cache.rb b/activesupport/lib/active_support/cache.rb index fa487060e2..3847d8b7ae 100644 --- a/activesupport/lib/active_support/cache.rb +++ b/activesupport/lib/active_support/cache.rb @@ -499,7 +499,7 @@ module ActiveSupport # The options hash is passed to the underlying cache implementation. # # All implementations may not support this method. - def clear + def clear(options = nil) raise NotImplementedError.new("#{self.class.name} does not support clear") end diff --git a/activesupport/lib/active_support/cache/file_store.rb b/activesupport/lib/active_support/cache/file_store.rb index d5c8585816..945f50a56e 100644 --- a/activesupport/lib/active_support/cache/file_store.rb +++ b/activesupport/lib/active_support/cache/file_store.rb @@ -27,7 +27,7 @@ module ActiveSupport # Deletes all items from the cache. In this case it deletes all the entries in the specified # file store directory except for .keep or .gitkeep. Be careful which directory is specified in your # config file when using +FileStore+ because everything in that directory will be deleted. - def clear + def clear(options = nil) root_dirs = exclude_from(cache_path, EXCLUDED_DIRS + GITKEEP_FILES) FileUtils.rm_r(root_dirs.collect { |f| File.join(cache_path, f) }) rescue Errno::ENOENT diff --git a/activesupport/lib/active_support/cache/strategy/local_cache.rb b/activesupport/lib/active_support/cache/strategy/local_cache.rb index 91875a56f5..0e4f383fb9 100644 --- a/activesupport/lib/active_support/cache/strategy/local_cache.rb +++ b/activesupport/lib/active_support/cache/strategy/local_cache.rb @@ -44,7 +44,7 @@ module ActiveSupport yield end - def clear + def clear(options = nil) @data.clear end @@ -79,9 +79,9 @@ module ActiveSupport local_cache_key) end - def clear # :nodoc: + def clear(options = nil) # :nodoc: return super unless cache = local_cache - cache.clear + cache.clear(options) super end -- cgit v1.2.3 From 52f90044a159559d94b4f597e9d79b44024ed09a Mon Sep 17 00:00:00 2001 From: Eugene Kenny Date: Sun, 14 May 2017 22:06:34 +0100 Subject: Cache: test coverage for cleanup behavior with local cache strategy No need to pass `#cleanup` options through to `LocalCache#clear`. Fixes #29081. References #25628. --- .../lib/active_support/cache/strategy/local_cache.rb | 2 +- .../test/cache/behaviors/local_cache_behavior.rb | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) (limited to 'activesupport') diff --git a/activesupport/lib/active_support/cache/strategy/local_cache.rb b/activesupport/lib/active_support/cache/strategy/local_cache.rb index 0e4f383fb9..69b3a93a05 100644 --- a/activesupport/lib/active_support/cache/strategy/local_cache.rb +++ b/activesupport/lib/active_support/cache/strategy/local_cache.rb @@ -87,7 +87,7 @@ module ActiveSupport def cleanup(options = nil) # :nodoc: return super unless cache = local_cache - cache.clear(options) + cache.clear super end diff --git a/activesupport/test/cache/behaviors/local_cache_behavior.rb b/activesupport/test/cache/behaviors/local_cache_behavior.rb index 3fb358bd0c..8530296374 100644 --- a/activesupport/test/cache/behaviors/local_cache_behavior.rb +++ b/activesupport/test/cache/behaviors/local_cache_behavior.rb @@ -17,6 +17,21 @@ module LocalCacheBehavior assert_nil @cache.read("foo") end + def test_cleanup_clears_local_cache_but_not_remote_cache + skip unless @cache.class.instance_methods(false).include?(:cleanup) + + @cache.with_local_cache do + @cache.write("foo", "bar") + assert_equal "bar", @cache.read("foo") + + @cache.send(:bypass_local_cache) { @cache.write("foo", "baz") } + assert_equal "bar", @cache.read("foo") + + @cache.cleanup + assert_equal "baz", @cache.read("foo") + end + end + def test_local_cache_of_write @cache.with_local_cache do @cache.write("foo", "bar") -- cgit v1.2.3 From 7440bf44baea53de950093ebf9ee4e8a3ed71066 Mon Sep 17 00:00:00 2001 From: Assain Date: Sat, 3 Jun 2017 01:21:10 +0530 Subject: set message_encryptor default cipher to aes-256-gcm - Introduce a method to select default cipher, and maintain backward compatibility --- activesupport/lib/active_support/message_encryptor.rb | 18 ++++++++++++++---- activesupport/lib/active_support/railtie.rb | 7 +++++++ 2 files changed, 21 insertions(+), 4 deletions(-) (limited to 'activesupport') diff --git a/activesupport/lib/active_support/message_encryptor.rb b/activesupport/lib/active_support/message_encryptor.rb index 726e1464ad..a24c557f1d 100644 --- a/activesupport/lib/active_support/message_encryptor.rb +++ b/activesupport/lib/active_support/message_encryptor.rb @@ -19,7 +19,17 @@ module ActiveSupport # encrypted_data = crypt.encrypt_and_sign('my secret data') # => "NlFBTTMwOUV5UlA1QlNEN2xkY2d6eThYWWh..." # crypt.decrypt_and_verify(encrypted_data) # => "my secret data" class MessageEncryptor - DEFAULT_CIPHER = "aes-256-cbc" + class << self + attr_accessor :use_authenticated_message_encryption #:nodoc: + + def default_cipher #:nodoc: + if use_authenticated_message_encryption + "aes-256-gcm" + else + "aes-256-cbc" + end + end + end module NullSerializer #:nodoc: def self.load(value) @@ -45,7 +55,7 @@ module ActiveSupport OpenSSLCipherError = OpenSSL::Cipher::CipherError # Initialize a new MessageEncryptor. +secret+ must be at least as long as - # the cipher key size. For the default 'aes-256-cbc' cipher, this is 256 + # the cipher key size. For the default 'aes-256-gcm' cipher, this is 256 # bits. If you are using a user-entered secret, you can generate a suitable # key by using ActiveSupport::KeyGenerator or a similar key # derivation function. @@ -66,7 +76,7 @@ module ActiveSupport sign_secret = signature_key_or_options.first @secret = secret @sign_secret = sign_secret - @cipher = options[:cipher] || DEFAULT_CIPHER + @cipher = options[:cipher] || self.class.default_cipher @digest = options[:digest] || "SHA1" unless aead_mode? @verifier = resolve_verifier @serializer = options[:serializer] || Marshal @@ -85,7 +95,7 @@ module ActiveSupport end # Given a cipher, returns the key length of the cipher to help generate the key of desired size - def self.key_len(cipher = DEFAULT_CIPHER) + def self.key_len(cipher = default_cipher) OpenSSL::Cipher.new(cipher).key_len end diff --git a/activesupport/lib/active_support/railtie.rb b/activesupport/lib/active_support/railtie.rb index 1b4ecf4d72..61d6e4aae3 100644 --- a/activesupport/lib/active_support/railtie.rb +++ b/activesupport/lib/active_support/railtie.rb @@ -7,6 +7,13 @@ module ActiveSupport config.eager_load_namespaces << ActiveSupport + initializer "active_support.set_authenticated_message_encryption" do |app| + if app.config.active_support.respond_to?(:use_authenticated_message_encryption) + ActiveSupport::MessageEncryptor.use_authenticated_message_encryption = + app.config.active_support.use_authenticated_message_encryption + end + end + initializer "active_support.reset_all_current_attributes_instances" do |app| app.reloader.before_class_unload { ActiveSupport::CurrentAttributes.clear_all } app.executor.to_run { ActiveSupport::CurrentAttributes.reset_all } -- cgit v1.2.3 From 36b349e387e3cf7e5f2976d2cad886d9ebf0b0b6 Mon Sep 17 00:00:00 2001 From: Kasper Timm Hansen Date: Sun, 11 Jun 2017 21:48:58 +0200 Subject: [ci skip] Add changelog entry for 6d402c6 [ Assain Jaleel & Kasper Timm Hansen ] --- activesupport/CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) (limited to 'activesupport') diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md index d1d61ac8d7..ab5237488a 100644 --- a/activesupport/CHANGELOG.md +++ b/activesupport/CHANGELOG.md @@ -1,3 +1,10 @@ +* Default `ActiveSupport::MessageEncryptor` to use AES 256 GCM encryption. + + On for new Rails 5.2 apps. Upgrading apps can find the config as a new + framework default. + + *Assain Jaleel* + * Cache: `write_multi` Rails.cache.write_multi foo: 'bar', baz: 'qux' -- cgit v1.2.3 From 9231d2a0b454d23ee7c735f9d1d6448396c94a55 Mon Sep 17 00:00:00 2001 From: "yuuji.yaginuma" Date: Mon, 12 Jun 2017 07:47:13 +0900 Subject: Fix `Message::Encryptor` default cipher [ci skip] Follow up of #29263 --- activesupport/lib/active_support/message_encryptor.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'activesupport') diff --git a/activesupport/lib/active_support/message_encryptor.rb b/activesupport/lib/active_support/message_encryptor.rb index a24c557f1d..e576766c64 100644 --- a/activesupport/lib/active_support/message_encryptor.rb +++ b/activesupport/lib/active_support/message_encryptor.rb @@ -67,7 +67,7 @@ module ActiveSupport # # Options: # * :cipher - Cipher to use. Can be any cipher returned by - # OpenSSL::Cipher.ciphers. Default is 'aes-256-cbc'. + # OpenSSL::Cipher.ciphers. Default is 'aes-256-gcm'. # * :digest - String of digest to use for signing. Default is # +SHA1+. Ignored when using an AEAD cipher like 'aes-256-gcm'. # * :serializer - Object serializer to use. Default is +Marshal+. -- cgit v1.2.3 From 6673cf7071094e87d473459452a2d0e4c2ccfebe Mon Sep 17 00:00:00 2001 From: bogdanvlviv Date: Sun, 11 Jun 2017 15:59:23 +0300 Subject: Use `require_relative` instead of `require` with full path --- activesupport/bin/test | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'activesupport') diff --git a/activesupport/bin/test b/activesupport/bin/test index a7beb14b27..470ce93f10 100755 --- a/activesupport/bin/test +++ b/activesupport/bin/test @@ -1,4 +1,4 @@ #!/usr/bin/env ruby COMPONENT_ROOT = File.expand_path("..", __dir__) -require File.expand_path("../tools/test", COMPONENT_ROOT) +require_relative "../../tools/test" -- cgit v1.2.3