# frozen_string_literal: true require_relative "abstract_unit" require "active_support/core_ext/kernel/singleton_class" module CallbacksTest class Record include ActiveSupport::Callbacks define_callbacks :save def self.before_save(*filters, &blk) set_callback(:save, :before, *filters, &blk) end def self.after_save(*filters, &blk) set_callback(:save, :after, *filters, &blk) end class << self def callback_symbol(callback_method) method_name = :"#{callback_method}_method" define_method(method_name) do history << [callback_method, :symbol] end method_name end def callback_proc(callback_method) Proc.new { |model| model.history << [callback_method, :proc] } end def callback_object(callback_method) klass = Class.new klass.define_method(callback_method) do |model| model.history << [:"#{callback_method}_save", :object] end klass.new end end def history @history ||= [] end end class CallbackClass def self.before(model) model.history << [:before_save, :class] end def self.after(model) model.history << [:after_save, :class] end end class Person < Record attr_accessor :save_fails [:before_save, :after_save].each do |callback_method| callback_method_sym = callback_method.to_sym send(callback_method, callback_symbol(callback_method_sym)) send(callback_method, callback_proc(callback_method_sym)) send(callback_method, callback_object(callback_method_sym.to_s.gsub(/_save/, ""))) send(callback_method, CallbackClass) send(callback_method) { |model| model.history << [callback_method_sym, :block] } end def save run_callbacks :save do raise "inside save" if save_fails end end end class PersonSkipper < Person skip_callback :save, :before, :before_save_method, if: :yes skip_callback :save, :after, :after_save_method, unless: :yes skip_callback :save, :after, :after_save_method, if: :no skip_callback :save, :before, :before_save_method, unless: :no skip_callback :save, :before, CallbackClass, if: :yes def yes; true; end def no; false; end end class PersonForProgrammaticSkipping < Person end class ParentController include ActiveSupport::Callbacks define_callbacks :dispatch set_callback :dispatch, :before, :log, unless: proc { |c| c.action_name == :index || c.action_name == :show } set_callback :dispatch, :after, :log2 attr_reader :action_name, :logger def initialize(action_name) @action_name, @logger = action_name, [] end def log @logger << action_name end def log2 @logger << action_name end def dispatch run_callbacks :dispatch do @logger << "Done" end self end end class Child < ParentController skip_callback :dispatch, :before, :log, if: proc { |c| c.action_name == :update } skip_callback :dispatch, :after, :log2 end class OneTimeCompile < Record @@starts_true, @@starts_false = true, false def initialize super end before_save Proc.new { |r| r.history << [:before_save, :starts_true, :if] }, if: :starts_true before_save Proc.new { |r| r.history << [:before_save, :starts_false, :if] }, if: :starts_false before_save Proc.new { |r| r.history << [:before_save, :starts_true, :unless] }, unless: :starts_true before_save Proc.new { |r| r.history << [:before_save, :starts_false, :unless] }, unless: :starts_false def starts_true if @@starts_true @@starts_true = false return true end @@starts_true end def starts_false unless @@starts_false @@starts_false = true return false end @@starts_false end def save run_callbacks :save end end class OneTimeCompileTest < ActiveSupport::TestCase def test_optimized_first_compile around = OneTimeCompile.new around.save assert_equal [ [:before_save, :starts_true, :if], [:before_save, :starts_true, :unless] ], around.history end end class AfterSaveConditionalPerson < Record after_save Proc.new { |r| r.history << [:after_save, :string1] } after_save Proc.new { |r| r.history << [:after_save, :string2] } def save run_callbacks :save end end class AfterSaveConditionalPersonCallbackTest < ActiveSupport::TestCase def test_after_save_runs_in_the_reverse_order person = AfterSaveConditionalPerson.new person.save assert_equal [ [:after_save, :string2], [:after_save, :string1] ], person.history end end class ConditionalPerson < Record # proc before_save Proc.new { |r| r.history << [:before_save, :proc] }, if: Proc.new { |r| true } before_save Proc.new { |r| r.history << "b00m" }, if: Proc.new { |r| false } before_save Proc.new { |r| r.history << [:before_save, :proc] }, unless: Proc.new { |r| false } before_save Proc.new { |r| r.history << "b00m" }, unless: Proc.new { |r| true } # symbol before_save Proc.new { |r| r.history << [:before_save, :symbol] }, if: :yes before_save Proc.new { |r| r.history << "b00m" }, if: :no before_save Proc.new { |r| r.history << [:before_save, :symbol] }, unless: :no before_save Proc.new { |r| r.history << "b00m" }, unless: :yes # Combined if and unless before_save Proc.new { |r| r.history << [:before_save, :combined_symbol] }, if: :yes, unless: :no before_save Proc.new { |r| r.history << "b00m" }, if: :yes, unless: :yes def yes; true; end def other_yes; true; end def no; false; end def other_no; false; end def save run_callbacks :save end end class CleanPerson < ConditionalPerson reset_callbacks :save end class MySuper include ActiveSupport::Callbacks define_callbacks :save end class MySlate < MySuper attr_reader :history attr_accessor :save_fails def initialize @history = [] end def save run_callbacks :save do raise "inside save" if save_fails @history << "running" end end def no; false; end def yes; true; end def method_missing(sym, *) case sym when /^log_(.*)/ @history << $1 nil when /^wrap_(.*)/ @history << "wrap_#$1" yield @history << "unwrap_#$1" nil when /^double_(.*)/ @history << "first_#$1" yield @history << "second_#$1" yield @history << "third_#$1" else super end end def respond_to_missing?(sym) sym.match?(/^(log|wrap)_/) || super end end class AroundPerson < MySlate set_callback :save, :before, :nope, if: :no set_callback :save, :before, :nope, unless: :yes set_callback :save, :after, :tweedle set_callback :save, :before, proc { |m| m.history << "yup" } set_callback :save, :before, :nope, if: proc { false } set_callback :save, :before, :nope, unless: proc { true } set_callback :save, :before, :yup, if: proc { true } set_callback :save, :before, :yup, unless: proc { false } set_callback :save, :around, :tweedle_dum set_callback :save, :around, :w0tyes, if: :yes set_callback :save, :around, :w0tno, if: :no set_callback :save, :around, :tweedle_deedle def nope @history << "boom" end def yup @history << "yup" end def w0tyes @history << "w0tyes before" yield @history << "w0tyes after" end def w0tno @history << "boom" yield end def tweedle_dum @history << "tweedle dum pre" yield @history << "tweedle dum post" end def tweedle @history << "tweedle" end def tweedle_deedle @history << "tweedle deedle pre" yield @history << "tweedle deedle post" end end class AroundPersonResult < MySuper attr_reader :result set_callback :save, :after, :tweedle_1 set_callback :save, :around, :tweedle_dum set_callback :save, :after, :tweedle_2 def tweedle_dum @result = yield end def tweedle_1 :tweedle_1 end def tweedle_2 :tweedle_2 end def save run_callbacks :save do :running end end end class HyphenatedCallbacks include ActiveSupport::Callbacks define_callbacks :save attr_reader :stuff set_callback :save, :before, :action, if: :yes def yes() true end def action @stuff = "ACTION" end def save run_callbacks :save do @stuff end end end module ExtendModule def self.extended(base) base.class_eval do set_callback :save, :before, :record3 end end def record3 @recorder << 3 end end module IncludeModule def self.included(base) base.class_eval do set_callback :save, :before, :record2 end end def record2 @recorder << 2 end end class ExtendCallbacks include ActiveSupport::Callbacks define_callbacks :save set_callback :save, :before, :record1 include IncludeModule def save run_callbacks :save end attr_reader :recorder def initialize @recorder = [] end private def record1 @recorder << 1 end end class AroundCallbacksTest < ActiveSupport::TestCase def test_save_around around = AroundPerson.new around.save assert_equal [ "yup", "yup", "tweedle dum pre", "w0tyes before", "tweedle deedle pre", "running", "tweedle deedle post", "w0tyes after", "tweedle dum post", "tweedle" ], around.history end end class DoubleYieldTest < ActiveSupport::TestCase class DoubleYieldModel < MySlate set_callback :save, :around, :wrap_outer set_callback :save, :around, :double_trouble set_callback :save, :around, :wrap_inner end def test_double_save double = DoubleYieldModel.new double.save assert_equal [ "wrap_outer", "first_trouble", "wrap_inner", "running", "unwrap_inner", "second_trouble", "wrap_inner", "running", "unwrap_inner", "third_trouble", "unwrap_outer", ], double.history end end class CallStackTest < ActiveSupport::TestCase def test_tidy_call_stack around = AroundPerson.new around.save_fails = true exception = (around.save rescue $!) # Make sure we have the exception we're expecting assert_equal "inside save", exception.message call_stack = exception.backtrace_locations call_stack.pop caller_locations(0).size # Yes, this looks like an implementation test, but it's the least # obtuse way of asserting that there aren't a load of entries in # the call stack for each callback. # # If you've renamed a method, or squeezed more lines out, go ahead # and update this assertion. But if you're here because a # refactoring added new lines, please reconsider. # As shown here, our current budget is one line for run_callbacks # itself, plus N+1 lines where N is the number of :around # callbacks that have been invoked, if there are any (plus # whatever the callbacks do themselves, of course). assert_equal [ "block in save", "block in run_callbacks", "tweedle_deedle", "block in run_callbacks", "w0tyes", "block in run_callbacks", "tweedle_dum", "block in run_callbacks", "run_callbacks", "save" ], call_stack.map(&:label) end def test_short_call_stack person = Person.new person.save_fails = true exception = (person.save rescue $!) # Make sure we have the exception we're expecting assert_equal "inside save", exception.message call_stack = exception.backtrace_locations call_stack.pop caller_locations(0).size # This budget much simpler: with no :around callbacks invoked, # there should be just one line. run_callbacks yields directly # back to its caller. assert_equal [ "block in save", "run_callbacks", "save" ], call_stack.map(&:label) end end class AroundCallbackResultTest < ActiveSupport::TestCase def test_save_around around = AroundPersonResult.new around.save assert_equal :running, around.result end end class SkipCallbacksTest < ActiveSupport::TestCase def test_skip_person person = PersonSkipper.new assert_equal [], person.history person.save assert_equal [ [:before_save, :proc], [:before_save, :object], [:before_save, :block], [:after_save, :block], [:after_save, :class], [:after_save, :object], [:after_save, :proc], [:after_save, :symbol] ], person.history end def test_skip_person_programmatically PersonForProgrammaticSkipping._save_callbacks.each do |save_callback| if "before" == save_callback.kind.to_s PersonForProgrammaticSkipping.skip_callback("save", save_callback.kind, save_callback.filter) end end person = PersonForProgrammaticSkipping.new assert_equal [], person.history person.save assert_equal [ [:after_save, :block], [:after_save, :class], [:after_save, :object], [:after_save, :proc], [:after_save, :symbol] ], person.history end end class CallbacksTest < ActiveSupport::TestCase def test_save_person person = Person.new assert_equal [], person.history person.save assert_equal [ [:before_save, :symbol], [:before_save, :proc], [:before_save, :object], [:before_save, :class], [:before_save, :block], [:after_save, :block], [:after_save, :class], [:after_save, :object], [:after_save, :proc], [:after_save, :symbol] ], person.history end end class ConditionalCallbackTest < ActiveSupport::TestCase def test_save_conditional_person person = ConditionalPerson.new person.save assert_equal [ [:before_save, :proc], [:before_save, :proc], [:before_save, :symbol], [:before_save, :symbol], [:before_save, :combined_symbol], ], person.history end end class ResetCallbackTest < ActiveSupport::TestCase def test_save_conditional_person person = CleanPerson.new person.save assert_equal [], person.history end end class AbstractCallbackTerminator include ActiveSupport::Callbacks def self.set_save_callbacks set_callback :save, :before, :first set_callback :save, :before, :second set_callback :save, :around, :around_it set_callback :save, :before, :third set_callback :save, :after, :first set_callback :save, :around, :around_it set_callback :save, :after, :third end attr_reader :history, :saved, :halted, :callback_name def initialize @history = [] end def around_it @history << "around1" yield @history << "around2" end def first @history << "first" end def second @history << "second" :halt end def third @history << "third" end def save run_callbacks :save do @saved = true end end def halted_callback_hook(filter, name) @halted = filter @callback_name = name end end class CallbackTerminator < AbstractCallbackTerminator define_callbacks :save, terminator: ->(_, result_lambda) { result_lambda.call == :halt } set_save_callbacks end class CallbackTerminatorSkippingAfterCallbacks < AbstractCallbackTerminator define_callbacks :save, terminator: ->(_, result_lambda) { result_lambda.call == :halt }, skip_after_callbacks_if_terminated: true set_save_callbacks end class CallbackDefaultTerminator < AbstractCallbackTerminator define_callbacks :save def second @history << "second" throw(:abort) end set_save_callbacks end class CallbackFalseTerminator < AbstractCallbackTerminator define_callbacks :save def second @history << "second" false end set_save_callbacks end class CallbackObject def before(caller) caller.record << "before" end def before_save(caller) caller.record << "before save" end def around(caller) caller.record << "around before" yield caller.record << "around after" end end class UsingObjectBefore include ActiveSupport::Callbacks define_callbacks :save set_callback :save, :before, CallbackObject.new attr_accessor :record def initialize @record = [] end def save run_callbacks :save do @record << "yielded" end end end class UsingObjectAround include ActiveSupport::Callbacks define_callbacks :save set_callback :save, :around, CallbackObject.new attr_accessor :record def initialize @record = [] end def save run_callbacks :save do @record << "yielded" end end end class CustomScopeObject include ActiveSupport::Callbacks define_callbacks :save, scope: [:kind, :name] set_callback :save, :before, CallbackObject.new attr_accessor :record def initialize @record = [] end def save run_callbacks :save do @record << "yielded" "CallbackResult" end end end class OneTwoThreeSave include ActiveSupport::Callbacks define_callbacks :save attr_accessor :record def initialize @record = [] end def save run_callbacks :save do @record << "yielded" end end def first @record << "one" end def second @record << "two" end def third @record << "three" end end class DuplicatingCallbacks < OneTwoThreeSave set_callback :save, :before, :first, :second set_callback :save, :before, :first, :third end class DuplicatingCallbacksInSameCall < OneTwoThreeSave set_callback :save, :before, :first, :second, :first, :third end class UsingObjectTest < ActiveSupport::TestCase def test_before_object u = UsingObjectBefore.new u.save assert_equal ["before", "yielded"], u.record end def test_around_object u = UsingObjectAround.new u.save assert_equal ["around before", "yielded", "around after"], u.record end def test_customized_object u = CustomScopeObject.new u.save assert_equal ["before save", "yielded"], u.record end def test_block_result_is_returned u = CustomScopeObject.new assert_equal "CallbackResult", u.save end end class CallbackTerminatorTest < ActiveSupport::TestCase def test_termination_skips_following_before_and_around_callbacks terminator = CallbackTerminator.new terminator.save assert_equal ["first", "second", "third", "first"], terminator.history end def test_termination_invokes_hook terminator = CallbackTerminator.new terminator.save assert_equal :second, terminator.halted assert_equal :save, terminator.callback_name end def test_block_never_called_if_terminated obj = CallbackTerminator.new obj.save assert_not obj.saved end end class CallbackTerminatorSkippingAfterCallbacksTest < ActiveSupport::TestCase def test_termination_skips_after_callbacks terminator = CallbackTerminatorSkippingAfterCallbacks.new terminator.save assert_equal ["first", "second"], terminator.history end end class CallbackDefaultTerminatorTest < ActiveSupport::TestCase def test_default_termination terminator = CallbackDefaultTerminator.new terminator.save assert_equal ["first", "second", "third", "first"], terminator.history end def test_default_termination_invokes_hook terminator = CallbackDefaultTerminator.new terminator.save assert_equal :second, terminator.halted end def test_block_never_called_if_abort_is_thrown obj = CallbackDefaultTerminator.new obj.save assert_not obj.saved end end class CallbackFalseTerminatorTest < ActiveSupport::TestCase def test_returning_false_does_not_halt_callback obj = CallbackFalseTerminator.new obj.save assert_nil obj.halted assert obj.saved end end class HyphenatedKeyTest < ActiveSupport::TestCase def test_save obj = HyphenatedCallbacks.new obj.save assert_equal "ACTION", obj.stuff end end class WriterSkipper < Person attr_accessor :age skip_callback :save, :before, :before_save_method, if: -> { age > 21 } end class WriterCallbacksTest < ActiveSupport::TestCase def test_skip_writer writer = WriterSkipper.new writer.age = 18 assert_equal [], writer.history writer.save assert_equal [ [:before_save, :symbol], [:before_save, :proc], [:before_save, :object], [:before_save, :class], [:before_save, :block], [:after_save, :block], [:after_save, :class], [:after_save, :object], [:after_save, :proc], [:after_save, :symbol] ], writer.history end end class ExtendCallbacksTest < ActiveSupport::TestCase def test_save model = ExtendCallbacks.new.extend ExtendModule model.save assert_equal [1, 2, 3], model.recorder end end class ExcludingDuplicatesCallbackTest < ActiveSupport::TestCase def test_excludes_duplicates_in_separate_calls model = DuplicatingCallbacks.new model.save assert_equal ["two", "one", "three", "yielded"], model.record end def test_excludes_duplicates_in_one_call model = DuplicatingCallbacksInSameCall.new model.save assert_equal ["two", "one", "three", "yielded"], model.record end end class CallbackProcTest < ActiveSupport::TestCase def build_class(callback) Class.new { include ActiveSupport::Callbacks define_callbacks :foo set_callback :foo, :before, callback def run; run_callbacks :foo; end } end def test_proc_arity_0 calls = [] klass = build_class(->() { calls << :foo }) klass.new.run assert_equal [:foo], calls end def test_proc_arity_1 calls = [] klass = build_class(->(o) { calls << o }) instance = klass.new instance.run assert_equal [instance], calls end def test_proc_arity_2 assert_raises(ArgumentError) do klass = build_class(->(x, y) { }) klass.new.run end end def test_proc_negative_called_with_empty_list calls = [] klass = build_class(->(*args) { calls << args }) klass.new.run assert_equal [[]], calls end end class ConditionalTests < ActiveSupport::TestCase def build_class(callback) Class.new { include ActiveSupport::Callbacks define_callbacks :foo set_callback :foo, :before, :foo, if: callback def foo; end def run; run_callbacks :foo; end } end # FIXME: do we really want to support classes as conditionals? There were # no tests for it previous to this. def test_class_conditional_with_scope z = [] callback = Class.new { define_singleton_method(:foo) { |o| z << o } } klass = Class.new { include ActiveSupport::Callbacks define_callbacks :foo, scope: [:name] set_callback :foo, :before, :foo, if: callback def run; run_callbacks :foo; end private def foo; end } object = klass.new object.run assert_equal [object], z end # FIXME: do we really want to support classes as conditionals? There were # no tests for it previous to this. def test_class z = [] klass = build_class Class.new { define_singleton_method(:before) { |o| z << o } } object = klass.new object.run assert_equal [object], z end def test_proc_negative_arity # passes an empty list if *args z = [] object = build_class(->(*args) { z << args }).new object.run assert_equal [], z.flatten end def test_proc_arity0 z = [] object = build_class(->() { z << 0 }).new object.run assert_equal [0], z end def test_proc_arity1 z = [] object = build_class(->(x) { z << x }).new object.run assert_equal [object], z end def test_proc_arity2 assert_raises(ArgumentError) do object = build_class(->(a, b) { }).new object.run end end end class ResetCallbackTest < ActiveSupport::TestCase def build_class(memo) klass = Class.new { include ActiveSupport::Callbacks define_callbacks :foo set_callback :foo, :before, :hello def run; run_callbacks :foo; end } klass.class_eval { define_method(:hello) { memo << :hi } } klass end def test_reset_callbacks events = [] klass = build_class events klass.new.run assert_equal 1, events.length klass.reset_callbacks :foo klass.new.run assert_equal 1, events.length end def test_reset_impacts_subclasses events = [] klass = build_class events subclass = Class.new(klass) { set_callback :foo, :before, :world } subclass.class_eval { define_method(:world) { events << :world } } subclass.new.run assert_equal 2, events.length klass.reset_callbacks :foo subclass.new.run assert_equal 3, events.length end end class CallbackTypeTest < ActiveSupport::TestCase def build_class(callback, n = 10) Class.new { include ActiveSupport::Callbacks define_callbacks :foo n.times { set_callback :foo, :before, callback } def run; run_callbacks :foo; end def self.skip(*things); skip_callback :foo, :before, *things; end } end def test_add_class calls = [] callback = Class.new { define_singleton_method(:before) { |o| calls << o } } build_class(callback).new.run assert_equal 10, calls.length end def test_add_lambda calls = [] build_class(->(o) { calls << o }).new.run assert_equal 10, calls.length end def test_add_symbol calls = [] klass = build_class(:bar) klass.class_eval { define_method(:bar) { calls << klass } } klass.new.run assert_equal 1, calls.length end def test_skip_class # removes one at a time calls = [] callback = Class.new { define_singleton_method(:before) { |o| calls << o } } klass = build_class(callback) 9.downto(0) { |i| klass.skip callback klass.new.run assert_equal i, calls.length calls.clear } end def test_skip_lambda # raises error calls = [] callback = ->(o) { calls << o } klass = build_class(callback) assert_raises(ArgumentError) { klass.skip callback } klass.new.run assert_equal 10, calls.length end def test_skip_symbol # removes all calls = [] klass = build_class(:bar) klass.class_eval { define_method(:bar) { calls << klass } } klass.skip :bar klass.new.run assert_equal 0, calls.length end def test_skip_string # raises error calls = [] klass = build_class(:bar) klass.class_eval { define_method(:bar) { calls << klass } } assert_raises(ArgumentError) { klass.skip "bar" } klass.new.run assert_equal 1, calls.length end def test_skip_undefined_callback # raises error calls = [] klass = build_class(:bar) klass.class_eval { define_method(:bar) { calls << klass } } assert_raises(ArgumentError) { klass.skip :qux } klass.new.run assert_equal 1, calls.length end def test_skip_without_raise # removes nothing calls = [] klass = build_class(:bar) klass.class_eval { define_method(:bar) { calls << klass } } klass.skip :qux, raise: false klass.new.run assert_equal 1, calls.length end end class NotSupportedStringConditionalTest < ActiveSupport::TestCase def test_string_conditional_options klass = Class.new(Record) assert_raises(ArgumentError) { klass.before_save :tweedle, if: ["true"] } assert_raises(ArgumentError) { klass.before_save :tweedle, if: "true" } assert_raises(ArgumentError) { klass.after_save :tweedle, unless: "false" } assert_raises(ArgumentError) { klass.skip_callback :save, :before, :tweedle, if: "true" } assert_raises(ArgumentError) { klass.skip_callback :save, :after, :tweedle, unless: "false" } end end class NotPermittedStringCallbackTest < ActiveSupport::TestCase def test_passing_string_callback_is_not_permitted klass = Class.new(Record) assert_raises(ArgumentError) do klass.before_save "tweedle" end end end end