diff options
Diffstat (limited to 'activesupport')
21 files changed, 267 insertions, 59 deletions
diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md index db9c7e96f3..4c7b134c35 100644 --- a/activesupport/CHANGELOG.md +++ b/activesupport/CHANGELOG.md @@ -1,3 +1,66 @@ +* Introduce `ActiveSupport::ActionableError`. + + Actionable errors let's you dispatch actions from Rails' error pages. This + can help you save time if you have a clear action for the resolution of + common development errors. + + The de-facto example are pending migrations. Every time pending migrations + are found, a middleware raises an error. With actionable errors, you can + run the migrations right from the error page. Other examples include Rails + plugins that need to run a rake task to setup themselves. They can now + raise actionable errors to run the setup straight from the error pages. + + Here is how to define an actionable error: + + ```ruby + class PendingMigrationError < MigrationError #:nodoc: + include ActiveSupport::ActionableError + + action "Run pending migrations" do + ActiveRecord::Tasks::DatabaseTasks.migrate + end + end + ``` + + To make an error actionable, include the `ActiveSupport::ActionableError` + module and invoke the `action` class macro to define the action. An action + needs a name and a procedure to execute. The name is shown as the name of a + button on the error pages. Once clicked, it will invoke the given + procedure. + + *Vipul A M*, *Yao Jie*, *Genadi Samokovarov* + +* Preserve `html_safe?` status on `ActiveSupport::SafeBuffer#*`. + + Before: + + ("<br />".html_safe * 2).html_safe? #=> nil + + After: + + ("<br />".html_safe * 2).html_safe? #=> true + + *Ryo Nakamura* + +* Calling test methods with `with_info_handler` method to allow minitest-hooks + plugin to work. + + *Mauri Mustonen* + +* The Zeitwerk compatibility interface for `ActiveSupport::Dependencies` no + longer implements `autoloaded_constants` or `autoloaded?` (undocumented, + anyway). Experience shows introspection does not have many use cases, and + troubleshooting is done by logging. With this design trade-off we are able + to use even less memory in all environments. + + *Xavier Noria* + +* Depends on Zeitwerk 2, which stores less metadata if reloading is disabled + and hence uses less memory when `config.cache_classes` is `true`, a standard + setup in production. + + *Xavier Noria* + * In `:zeitwerk` mode, eager load directories in engines and applications only if present in their respective `config.eager_load_paths`. diff --git a/activesupport/activesupport.gemspec b/activesupport/activesupport.gemspec index bf0fe0f76d..7ecbafc911 100644 --- a/activesupport/activesupport.gemspec +++ b/activesupport/activesupport.gemspec @@ -34,5 +34,5 @@ Gem::Specification.new do |s| s.add_dependency "tzinfo", "~> 1.1" s.add_dependency "minitest", "~> 5.1" s.add_dependency "concurrent-ruby", "~> 1.0", ">= 1.0.2" - s.add_dependency "zeitwerk", "~> 1.4", ">= 1.4.3" + s.add_dependency "zeitwerk", "~> 2.1", ">= 2.1.4" end diff --git a/activesupport/lib/active_support.rb b/activesupport/lib/active_support.rb index 5589c71281..9e242ddeaa 100644 --- a/activesupport/lib/active_support.rb +++ b/activesupport/lib/active_support.rb @@ -34,6 +34,7 @@ module ActiveSupport extend ActiveSupport::Autoload autoload :Concern + autoload :ActionableError autoload :CurrentAttributes autoload :Dependencies autoload :DescendantsTracker diff --git a/activesupport/lib/active_support/actionable_error.rb b/activesupport/lib/active_support/actionable_error.rb new file mode 100644 index 0000000000..7db14cd178 --- /dev/null +++ b/activesupport/lib/active_support/actionable_error.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +module ActiveSupport + # Actionable errors let's you define actions to resolve an error. + # + # To make an error actionable, include the <tt>ActiveSupport::ActionableError</tt> + # module and invoke the +action+ class macro to define the action. An action + # needs a name and a block to execute. + module ActionableError + extend Concern + + class NonActionable < StandardError; end + + included do + class_attribute :_actions, default: {} + end + + def self.actions(error) # :nodoc: + case error + when ActionableError, -> it { Class === it && it < ActionableError } + error._actions + else + {} + end + end + + def self.dispatch(error, name) # :nodoc: + actions(error).fetch(name).call + rescue KeyError + raise NonActionable, "Cannot find action \"#{name}\"" + end + + module ClassMethods + # Defines an action that can resolve the error. + # + # class PendingMigrationError < MigrationError + # include ActiveSupport::ActionableError + # + # action "Run pending migrations" do + # ActiveRecord::Tasks::DatabaseTasks.migrate + # end + # end + def action(name, &block) + _actions[name] = block + end + end + end +end diff --git a/activesupport/lib/active_support/backtrace_cleaner.rb b/activesupport/lib/active_support/backtrace_cleaner.rb index 62973eca58..02cbfbaee6 100644 --- a/activesupport/lib/active_support/backtrace_cleaner.rb +++ b/activesupport/lib/active_support/backtrace_cleaner.rb @@ -122,7 +122,11 @@ module ActiveSupport end def noise(backtrace) - backtrace - silence(backtrace) + backtrace.select do |line| + @silencers.any? do |s| + s.call(line) + end + end end end end diff --git a/activesupport/lib/active_support/cache/redis_cache_store.rb b/activesupport/lib/active_support/cache/redis_cache_store.rb index 9a55e49e27..bb092fcde9 100644 --- a/activesupport/lib/active_support/cache/redis_cache_store.rb +++ b/activesupport/lib/active_support/cache/redis_cache_store.rb @@ -152,12 +152,14 @@ module ActiveSupport # Creates a new Redis cache store. # - # Handles three options: block provided to instantiate, single URL - # provided, and multiple URLs provided. + # Handles four options: :redis block, :redis instance, single :url + # string, and multiple :url strings. # - # :redis Proc -> options[:redis].call - # :url String -> Redis.new(url: …) - # :url Array -> Redis::Distributed.new([{ url: … }, { url: … }, …]) + # Option Class Result + # :redis Proc -> options[:redis].call + # :redis Object -> options[:redis] + # :url String -> Redis.new(url: …) + # :url Array -> Redis::Distributed.new([{ url: … }, { url: … }, …]) # # No namespace is set by default. Provide one if the Redis cache # server is shared with other apps: <tt>namespace: 'myapp-cache'</tt>. @@ -361,6 +363,7 @@ module ActiveSupport def read_multi_mget(*names) options = names.extract_options! options = merged_options(options) + return {} if names == [] keys = names.map { |name| normalize_key(name, options) } diff --git a/activesupport/lib/active_support/concern.rb b/activesupport/lib/active_support/concern.rb index 5d356a0ab6..708c445031 100644 --- a/activesupport/lib/active_support/concern.rb +++ b/activesupport/lib/active_support/concern.rb @@ -110,7 +110,7 @@ module ActiveSupport base.instance_variable_set(:@_dependencies, []) end - def append_features(base) + def append_features(base) #:nodoc: if base.instance_variable_defined?(:@_dependencies) base.instance_variable_get(:@_dependencies) << self false @@ -123,6 +123,9 @@ module ActiveSupport end end + # Evaluate given block in context of base class, + # so that you can write class macros here. + # When you define more than one +included+ block, it raises an exception. def included(base = nil, &block) if base.nil? if instance_variable_defined?(:@_included_block) @@ -137,6 +140,26 @@ module ActiveSupport end end + # Define class methods from given block. + # You can define private class methods as well. + # + # module Example + # extend ActiveSupport::Concern + # + # class_methods do + # def foo; puts 'foo'; end + # + # private + # def bar; puts 'bar'; end + # end + # end + # + # class Buzz + # include Example + # end + # + # Buzz.foo # => "foo" + # Buzz.bar # => private method 'bar' called for Buzz:Class(NoMethodError) def class_methods(&class_methods_module_definition) mod = const_defined?(:ClassMethods, false) ? const_get(:ClassMethods) : diff --git a/activesupport/lib/active_support/core_ext/string/output_safety.rb b/activesupport/lib/active_support/core_ext/string/output_safety.rb index 638152626b..645b1fea17 100644 --- a/activesupport/lib/active_support/core_ext/string/output_safety.rb +++ b/activesupport/lib/active_support/core_ext/string/output_safety.rb @@ -213,6 +213,12 @@ module ActiveSupport #:nodoc: dup.concat(other) end + def *(*) + new_safe_buffer = super + new_safe_buffer.instance_variable_set(:@html_safe, @html_safe) + new_safe_buffer + end + def %(args) case args when Hash diff --git a/activesupport/lib/active_support/dependencies/zeitwerk_integration.rb b/activesupport/lib/active_support/dependencies/zeitwerk_integration.rb index a43d03cf09..fd39ad6e55 100644 --- a/activesupport/lib/active_support/dependencies/zeitwerk_integration.rb +++ b/activesupport/lib/active_support/dependencies/zeitwerk_integration.rb @@ -10,6 +10,8 @@ module ActiveSupport def clear Dependencies.unload_interlock do Rails.autoloaders.main.reload + rescue Zeitwerk::ReloadingDisabledError + raise "reloading is disabled because config.cache_classes is true" end end @@ -22,16 +24,12 @@ module ActiveSupport end def autoloaded_constants - cpaths = [] - Rails.autoloaders.each do |autoloader| - cpaths.concat(autoloader.loaded_cpaths.to_a) - end - cpaths + Rails.autoloaders.main.unloadable_cpaths end def autoloaded?(object) cpath = object.is_a?(Module) ? object.name : object.to_s - Rails.autoloaders.any? { |autoloader| autoloader.loaded?(cpath) } + Rails.autoloaders.main.unloadable_cpath?(cpath) end def verbose=(verbose) @@ -51,15 +49,15 @@ module ActiveSupport end class << self - def take_over - setup_autoloaders + def take_over(enable_reloading:) + setup_autoloaders(enable_reloading) freeze_paths decorate_dependencies end private - def setup_autoloaders + def setup_autoloaders(enable_reloading) Dependencies.autoload_paths.each do |autoload_path| # Zeitwerk only accepts existing directories in `push_dir` to # prevent misconfigurations. @@ -72,6 +70,7 @@ module ActiveSupport autoloader.do_not_eager_load(autoload_path) unless eager_load?(autoload_path) end + Rails.autoloaders.main.enable_reloading if enable_reloading Rails.autoloaders.each(&:setup) end diff --git a/activesupport/lib/active_support/deprecation/method_wrappers.rb b/activesupport/lib/active_support/deprecation/method_wrappers.rb index d99571790f..7c0a54a1d0 100644 --- a/activesupport/lib/active_support/deprecation/method_wrappers.rb +++ b/activesupport/lib/active_support/deprecation/method_wrappers.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require "active_support/core_ext/array/extract_options" +require "active_support/core_ext/module/redefine_method" module ActiveSupport class Deprecation @@ -52,29 +53,17 @@ module ActiveSupport options = method_names.extract_options! deprecator = options.delete(:deprecator) || self method_names += options.keys - mod = Module.new + mod = nil method_names.each do |method_name| if target_module.method_defined?(method_name) || target_module.private_method_defined?(method_name) - aliased_method, punctuation = method_name.to_s.sub(/([?!=])$/, ""), $1 - with_method = "#{aliased_method}_with_deprecation#{punctuation}" - without_method = "#{aliased_method}_without_deprecation#{punctuation}" - - target_module.define_method(with_method) do |*args, &block| + method = target_module.instance_method(method_name) + target_module.redefine_method(method_name) do |*args, &block| deprecator.deprecation_warning(method_name, options[method_name]) - send(without_method, *args, &block) - end - - target_module.alias_method(without_method, method_name) - target_module.alias_method(method_name, with_method) - - case - when target_module.protected_method_defined?(without_method) - target_module.send(:protected, method_name) - when target_module.private_method_defined?(without_method) - target_module.send(:private, method_name) + method.bind(self).call(*args, &block) end else + mod ||= Module.new mod.define_method(method_name) do |*args, &block| deprecator.deprecation_warning(method_name, options[method_name]) super(*args, &block) @@ -82,7 +71,7 @@ module ActiveSupport end end - target_module.prepend(mod) unless mod.instance_methods(false).empty? + target_module.prepend(mod) if mod end end end diff --git a/activesupport/lib/active_support/descendants_tracker.rb b/activesupport/lib/active_support/descendants_tracker.rb index 2dca990712..21565138a7 100644 --- a/activesupport/lib/active_support/descendants_tracker.rb +++ b/activesupport/lib/active_support/descendants_tracker.rb @@ -23,10 +23,10 @@ module ActiveSupport def clear if defined? ActiveSupport::Dependencies @@direct_descendants.each do |klass, descendants| - if ActiveSupport::Dependencies.autoloaded?(klass) + if Dependencies.autoloaded?(klass) @@direct_descendants.delete(klass) else - descendants.reject! { |v| ActiveSupport::Dependencies.autoloaded?(v) } + descendants.reject! { |v| Dependencies.autoloaded?(v) } end end else diff --git a/activesupport/lib/active_support/duration.rb b/activesupport/lib/active_support/duration.rb index 97b4634d7b..a30bd11a87 100644 --- a/activesupport/lib/active_support/duration.rb +++ b/activesupport/lib/active_support/duration.rb @@ -4,7 +4,6 @@ require "active_support/core_ext/array/conversions" require "active_support/core_ext/module/delegation" require "active_support/core_ext/object/acts_like" require "active_support/core_ext/string/filters" -require "active_support/deprecation" module ActiveSupport # Provides accurate date and time measurements using Date#advance and diff --git a/activesupport/lib/active_support/i18n_railtie.rb b/activesupport/lib/active_support/i18n_railtie.rb index 584930e413..8faa93a3e4 100644 --- a/activesupport/lib/active_support/i18n_railtie.rb +++ b/activesupport/lib/active_support/i18n_railtie.rb @@ -97,7 +97,8 @@ module I18n If you desire the default locale to be included in the defaults, please explicitly configure it with `config.i18n.fallbacks.defaults = [I18n.default_locale]` or `config.i18n.fallbacks = [I18n.default_locale, - {...}]` + {...}]`. If you want to opt-in to the new behavior, use + `config.i18n.fallbacks.defaults = [nil, {...}]`. MSG args.unshift I18n.default_locale end diff --git a/activesupport/lib/active_support/notifications/fanout.rb b/activesupport/lib/active_support/notifications/fanout.rb index c506b35b1e..8812b67f63 100644 --- a/activesupport/lib/active_support/notifications/fanout.rb +++ b/activesupport/lib/active_support/notifications/fanout.rb @@ -180,13 +180,13 @@ module ActiveSupport def start(name, id, payload) timestack = Thread.current[:_timestack] ||= [] - timestack.push Time.now + timestack.push Concurrent.monotonic_time end def finish(name, id, payload) timestack = Thread.current[:_timestack] started = timestack.pop - @delegate.call(name, started, Time.now, id, payload) + @delegate.call(name, started, Concurrent.monotonic_time, id, payload) end end diff --git a/activesupport/lib/active_support/notifications/instrumenter.rb b/activesupport/lib/active_support/notifications/instrumenter.rb index a03e7e483e..12546511a8 100644 --- a/activesupport/lib/active_support/notifications/instrumenter.rb +++ b/activesupport/lib/active_support/notifications/instrumenter.rb @@ -68,9 +68,8 @@ module ActiveSupport @transaction_id = transaction_id @end = ending @children = [] - @duration = nil - @cpu_time_start = nil - @cpu_time_finish = nil + @cpu_time_start = 0 + @cpu_time_finish = 0 @allocation_count_start = 0 @allocation_count_finish = 0 end @@ -125,7 +124,7 @@ module ActiveSupport # # @event.duration # => 1000.138 def duration - @duration ||= 1000.0 * (self.end - time) + 1000.0 * (self.end - time) end def <<(event) diff --git a/activesupport/lib/active_support/testing/parallelization.rb b/activesupport/lib/active_support/testing/parallelization.rb index 63440069b1..e760bf5ce3 100644 --- a/activesupport/lib/active_support/testing/parallelization.rb +++ b/activesupport/lib/active_support/testing/parallelization.rb @@ -71,7 +71,9 @@ module ActiveSupport fork do DRb.stop_service - after_fork(worker) + begin + after_fork(worker) + rescue => setup_exception; end queue = DRbObject.new_with_uri(@url) @@ -79,7 +81,11 @@ module ActiveSupport klass = job[0] method = job[1] reporter = job[2] - result = Minitest.run_one_method(klass, method) + result = klass.with_info_handler reporter do + Minitest.run_one_method(klass, method) + end + + add_setup_exception(result, setup_exception) if setup_exception begin queue.record(reporter, result) @@ -104,6 +110,11 @@ module ActiveSupport @queue_size.times { @queue << nil } @pool.each { |pid| Process.waitpid pid } end + + private + def add_setup_exception(result, setup_exception) + result.failures.prepend Minitest::UnexpectedError.new(setup_exception) + end end end end diff --git a/activesupport/test/actionable_error_test.rb b/activesupport/test/actionable_error_test.rb new file mode 100644 index 0000000000..63046b937c --- /dev/null +++ b/activesupport/test/actionable_error_test.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +require "abstract_unit" +require "active_support/actionable_error" + +class ActionableErrorTest < ActiveSupport::TestCase + NonActionableError = Class.new(StandardError) + + class DispatchableError < StandardError + include ActiveSupport::ActionableError + + class_attribute :flip1, default: false + class_attribute :flip2, default: false + + action "Flip 1" do + self.flip1 = true + end + + action "Flip 2" do + self.flip2 = true + end + end + + test "returns all action of an actionable error" do + assert_equal ["Flip 1", "Flip 2"], ActiveSupport::ActionableError.actions(DispatchableError).keys + assert_equal ["Flip 1", "Flip 2"], ActiveSupport::ActionableError.actions(DispatchableError.new).keys + end + + test "returns no actions for non-actionable errors" do + assert ActiveSupport::ActionableError.actions(Exception).empty? + assert ActiveSupport::ActionableError.actions(Exception.new).empty? + end + + test "dispatches actions from error and name" do + assert_changes "DispatchableError.flip1", from: false, to: true do + ActiveSupport::ActionableError.dispatch DispatchableError, "Flip 1" + end + end + + test "cannot dispatch missing actions" do + err = assert_raises ActiveSupport::ActionableError::NonActionable do + ActiveSupport::ActionableError.dispatch NonActionableError, "action" + end + + assert_equal 'Cannot find action "action"', err.to_s + end +end diff --git a/activesupport/test/cache/stores/redis_cache_store_test.rb b/activesupport/test/cache/stores/redis_cache_store_test.rb index 1d87f74347..790534cd3c 100644 --- a/activesupport/test/cache/stores/redis_cache_store_test.rb +++ b/activesupport/test/cache/stores/redis_cache_store_test.rb @@ -140,6 +140,12 @@ module ActiveSupport::Cache::RedisCacheStoreTests end end + def test_fetch_multi_without_names + assert_not_called(@cache.redis, :mget) do + @cache.fetch_multi() { } + end + end + def test_increment_expires_in assert_called_with @cache.redis, :incrby, [ "#{@namespace}:foo", 1 ] do assert_called_with @cache.redis, :expire, [ "#{@namespace}:foo", 60 ] do diff --git a/activesupport/test/deprecation/method_wrappers_test.rb b/activesupport/test/deprecation/method_wrappers_test.rb index 18729941bc..0aa3233aab 100644 --- a/activesupport/test/deprecation/method_wrappers_test.rb +++ b/activesupport/test/deprecation/method_wrappers_test.rb @@ -89,12 +89,4 @@ class MethodWrappersTest < ActiveSupport::TestCase warning = /old_method is deprecated and will be removed from Rails \d.\d \(use new_method instead\)/ assert_deprecated(warning) { assert_equal "abc", @klass.old_method } end - - def test_method_with_without_deprecation_is_exposed - ActiveSupport::Deprecation.deprecate_methods(@klass, old_method: :new_method) - - warning = /old_method is deprecated and will be removed from Rails \d.\d \(use new_method instead\)/ - assert_deprecated(warning) { assert_equal "abc", @klass.new.old_method_with_deprecation } - assert_equal "abc", @klass.new.old_method_without_deprecation - end end diff --git a/activesupport/test/notifications_test.rb b/activesupport/test/notifications_test.rb index bb20d26a25..0af59764b5 100644 --- a/activesupport/test/notifications_test.rb +++ b/activesupport/test/notifications_test.rb @@ -302,7 +302,7 @@ module Notifications class EventTest < TestCase def test_events_are_initialized_with_details - time = Time.now + time = Concurrent.monotonic_time event = event(:foo, time, time + 0.01, random_id, {}) assert_equal :foo, event.name @@ -310,15 +310,24 @@ module Notifications assert_in_delta 10.0, event.duration, 0.00001 end + def test_event_cpu_time_and_idle_time_when_start_and_finish_is_not_called + time = Concurrent.monotonic_time + event = event(:foo, time, time + 0.01, random_id, {}) + + assert_equal 0, event.cpu_time + assert_in_delta 10.0, event.idle_time, 0.00001 + end + + def test_events_consumes_information_given_as_payload - event = event(:foo, Time.now, Time.now + 1, random_id, payload: :bar) + event = event(:foo, Concurrent.monotonic_time, Concurrent.monotonic_time + 1, random_id, payload: :bar) assert_equal Hash[payload: :bar], event.payload end def test_event_is_parent_based_on_children - time = Time.utc(2009, 01, 01, 0, 0, 1) + time = Concurrent.monotonic_time - parent = event(:foo, Time.utc(2009), Time.utc(2009) + 100, random_id, {}) + parent = event(:foo, Concurrent.monotonic_time, Concurrent.monotonic_time + 100, random_id, {}) child = event(:foo, time, time + 10, random_id, {}) not_child = event(:foo, time, time + 100, random_id, {}) diff --git a/activesupport/test/safe_buffer_test.rb b/activesupport/test/safe_buffer_test.rb index b1a1c2d390..f475e05c9a 100644 --- a/activesupport/test/safe_buffer_test.rb +++ b/activesupport/test/safe_buffer_test.rb @@ -150,6 +150,14 @@ class SafeBufferTest < ActiveSupport::TestCase assert_equal "hello<>", clean + @buffer end + test "Should preserve html_safe? status on multiplication" do + multiplied_safe_buffer = "<br />".html_safe * 2 + assert_predicate multiplied_safe_buffer, :html_safe? + + multiplied_unsafe_buffer = @buffer.gsub("", "<>") * 2 + assert_not_predicate multiplied_unsafe_buffer, :html_safe? + end + test "Should concat as a normal string when safe" do clean = "hello".html_safe @buffer.gsub!("", "<>") |