mirror of
https://github.com/rails/rails.git
synced 2022-11-09 12:12:34 -05:00
d5ac941ddc
I'm writing this patch for two purposes: 1. I want to reduce the number of times `object_id` is called. Calling `object_id` can have negative impacts on performance in Ruby 2.7+, so it would be nice to stop calling it. 2. I'm not sure why we're treating lambdas specially here. It looks like we wanted to prevent people from skipping callbacks that were defined with a lambda, but I think that is silly. If the user has a reference to a lambda, and they want to skip it, we should let them. I think this cleans up some code, helps with performance, and is a more intuitive interface.
1190 lines
29 KiB
Ruby
1190 lines
29 KiB
Ruby
# 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
|
|
public_send(callback_method, callback_symbol(callback_method_sym))
|
|
public_send(callback_method, callback_proc(callback_method_sym))
|
|
public_send(callback_method, callback_object(callback_method_sym.to_s.gsub(/_save/, "")))
|
|
public_send(callback_method, CallbackClass)
|
|
public_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_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
|