2f19782dce
It's a common useful pattern for situation where something isn't supposed to happen, but if it does we can recover from it. So in such situation you don't want such issue to be hidden in development or test, as it's likely a bug, but do not want to fail a request if it happens in production. In other words, it behaves like `#record` in development and test environments, and like `raise` in production. Fix: https://github.com/rails/rails/pull/49638 Fix: https://github.com/rails/rails/pull/49339 Co-Authored-By: Andrew Novoselac <andrew.novoselac@shopify.com> Co-Authored-By: Dustin Brown <dbrown9@gmail.com>
298 lines
8.9 KiB
Ruby
298 lines
8.9 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require_relative "abstract_unit"
|
|
require "active_support/execution_context/test_helper"
|
|
require "active_support/error_reporter/test_helper"
|
|
|
|
class ErrorReporterTest < ActiveSupport::TestCase
|
|
# ExecutionContext is automatically reset in Rails app via executor hooks set in railtie
|
|
# But not in Active Support's own test suite.
|
|
include ActiveSupport::ExecutionContext::TestHelper
|
|
include ActiveSupport::ErrorReporter::TestHelper
|
|
|
|
setup do
|
|
@reporter = ActiveSupport::ErrorReporter.new
|
|
@subscriber = ActiveSupport::ErrorReporter::TestHelper::ErrorSubscriber.new
|
|
@reporter.subscribe(@subscriber)
|
|
@error = ArgumentError.new("Oops")
|
|
end
|
|
|
|
test "receives the execution context" do
|
|
@reporter.set_context(section: "admin")
|
|
error = ArgumentError.new("Oops")
|
|
@reporter.report(error, handled: true)
|
|
assert_equal [[error, true, :warning, "application", { section: "admin" }]], @subscriber.events
|
|
end
|
|
|
|
test "passed context has priority over the execution context" do
|
|
@reporter.set_context(section: "admin")
|
|
error = ArgumentError.new("Oops")
|
|
@reporter.report(error, handled: true, context: { section: "public" })
|
|
assert_equal [[error, true, :warning, "application", { section: "public" }]], @subscriber.events
|
|
end
|
|
|
|
test "passed source is forwarded" do
|
|
error = ArgumentError.new("Oops")
|
|
@reporter.report(error, handled: true, source: "my_gem")
|
|
assert_equal [[error, true, :warning, "my_gem", {}]], @subscriber.events
|
|
end
|
|
|
|
test "#disable allow to skip a subscriber" do
|
|
@reporter.disable(@subscriber) do
|
|
@reporter.report(ArgumentError.new("Oops"), handled: true)
|
|
end
|
|
assert_equal [], @subscriber.events
|
|
end
|
|
|
|
test "#disable allow to skip a subscribers per class" do
|
|
@reporter.disable(ErrorSubscriber) do
|
|
@reporter.report(ArgumentError.new("Oops"), handled: true)
|
|
end
|
|
assert_equal [], @subscriber.events
|
|
end
|
|
|
|
test "#handle swallow and report any unhandled error" do
|
|
error = ArgumentError.new("Oops")
|
|
@reporter.handle do
|
|
raise error
|
|
end
|
|
assert_equal [[error, true, :warning, "application", {}]], @subscriber.events
|
|
end
|
|
|
|
test "#handle can be scoped to an exception class" do
|
|
assert_raises ArgumentError do
|
|
@reporter.handle(NameError) do
|
|
raise ArgumentError
|
|
end
|
|
end
|
|
assert_equal [], @subscriber.events
|
|
end
|
|
|
|
test "#handle can be scoped to several exception classes" do
|
|
assert_raises ArgumentError do
|
|
@reporter.handle(NameError, NoMethodError) do
|
|
raise ArgumentError
|
|
end
|
|
end
|
|
assert_equal [], @subscriber.events
|
|
end
|
|
|
|
test "#handle swallows and reports matching errors" do
|
|
error = ArgumentError.new("Oops")
|
|
@reporter.handle(NameError, ArgumentError) do
|
|
raise error
|
|
end
|
|
assert_equal [[error, true, :warning, "application", {}]], @subscriber.events
|
|
end
|
|
|
|
test "#handle passes through the return value" do
|
|
result = @reporter.handle do
|
|
2 + 2
|
|
end
|
|
assert_equal 4, result
|
|
end
|
|
|
|
test "#handle returns nil on handled raise" do
|
|
result = @reporter.handle do
|
|
raise StandardError
|
|
2 + 2
|
|
end
|
|
assert_nil result
|
|
end
|
|
|
|
test "#handle returns the value of the fallback as a proc on handled raise" do
|
|
result = @reporter.handle(fallback: -> { 2 + 2 }) do
|
|
raise StandardError
|
|
end
|
|
assert_equal 4, result
|
|
end
|
|
|
|
test "#handle raises if the fallback is not a callable" do
|
|
assert_raises NoMethodError do
|
|
@reporter.handle(fallback: "four") do
|
|
raise StandardError
|
|
end
|
|
end
|
|
end
|
|
|
|
test "#handle raises the error up if fallback is a proc that then also raises" do
|
|
assert_raises ArgumentError do
|
|
@reporter.handle(fallback: -> { raise ArgumentError }) do
|
|
raise StandardError
|
|
end
|
|
end
|
|
end
|
|
|
|
test "#record report any unhandled error and re-raise them" do
|
|
error = ArgumentError.new("Oops")
|
|
assert_raises ArgumentError do
|
|
@reporter.record do
|
|
raise error
|
|
end
|
|
end
|
|
assert_equal [[error, false, :error, "application", {}]], @subscriber.events
|
|
end
|
|
|
|
test "#record can be scoped to an exception class" do
|
|
assert_raises ArgumentError do
|
|
@reporter.record(NameError) do
|
|
raise ArgumentError
|
|
end
|
|
end
|
|
assert_equal [], @subscriber.events
|
|
end
|
|
|
|
test "#record can be scoped to several exception classes" do
|
|
assert_raises ArgumentError do
|
|
@reporter.record(NameError, NoMethodError) do
|
|
raise ArgumentError
|
|
end
|
|
end
|
|
assert_equal [], @subscriber.events
|
|
end
|
|
|
|
test "#record report any matching, unhandled error and re-raise them" do
|
|
error = ArgumentError.new("Oops")
|
|
assert_raises ArgumentError do
|
|
@reporter.record(NameError, ArgumentError) do
|
|
raise error
|
|
end
|
|
end
|
|
assert_equal [[error, false, :error, "application", {}]], @subscriber.events
|
|
end
|
|
|
|
test "#record passes through the return value" do
|
|
result = @reporter.record do
|
|
2 + 2
|
|
end
|
|
assert_equal 4, result
|
|
end
|
|
|
|
test "#unexpected swallows errors by default" do
|
|
error = RuntimeError.new("Oops")
|
|
assert_nil @reporter.unexpected(error)
|
|
assert_equal [[error, true, :warning, "application", {}]], @subscriber.events
|
|
assert_not_predicate error.backtrace, :empty?
|
|
end
|
|
|
|
test "#unexpected accepts an error message" do
|
|
assert_nil @reporter.unexpected("Oops")
|
|
assert_equal 1, @subscriber.events.size
|
|
|
|
error, *event_details = @subscriber.events.first
|
|
assert_equal [true, :warning, "application", {}], event_details
|
|
|
|
assert_equal "Oops", error.message
|
|
assert_equal RuntimeError, error.class
|
|
assert_not_predicate error.backtrace, :empty?
|
|
end
|
|
|
|
test "#unexpected re-raise errors in development and test" do
|
|
@reporter.debug_mode = true
|
|
error = RuntimeError.new("Oops")
|
|
raise_line = __LINE__ + 2
|
|
raised_error = assert_raises ActiveSupport::ErrorReporter::UnexpectedError do
|
|
@reporter.unexpected(error)
|
|
end
|
|
assert_includes raised_error.message, "RuntimeError: Oops"
|
|
assert_not_nil raised_error.cause
|
|
assert_same error, raised_error.cause
|
|
assert_includes raised_error.backtrace.first, "#{__FILE__}:#{raise_line}"
|
|
end
|
|
|
|
test "can have multiple subscribers" do
|
|
second_subscriber = ErrorSubscriber.new
|
|
@reporter.subscribe(second_subscriber)
|
|
|
|
error = ArgumentError.new("Oops")
|
|
@reporter.report(error, handled: true)
|
|
|
|
assert_equal 1, @subscriber.events.size
|
|
assert_equal 1, second_subscriber.events.size
|
|
end
|
|
|
|
test "can unsubscribe" do
|
|
second_subscriber = ErrorSubscriber.new
|
|
@reporter.subscribe(second_subscriber)
|
|
|
|
error = ArgumentError.new("Oops")
|
|
@reporter.report(error, handled: true)
|
|
|
|
@reporter.unsubscribe(second_subscriber)
|
|
|
|
error = ArgumentError.new("Oops 2")
|
|
@reporter.report(error, handled: true)
|
|
|
|
assert_equal 2, @subscriber.events.size
|
|
assert_equal 1, second_subscriber.events.size
|
|
|
|
@reporter.subscribe(second_subscriber)
|
|
@reporter.unsubscribe(ErrorSubscriber)
|
|
|
|
error = ArgumentError.new("Oops 3")
|
|
@reporter.report(error, handled: true)
|
|
|
|
assert_equal 2, @subscriber.events.size
|
|
assert_equal 1, second_subscriber.events.size
|
|
end
|
|
|
|
test "handled errors default to :warning severity" do
|
|
@reporter.report(@error, handled: true)
|
|
assert_equal :warning, @subscriber.events.dig(0, 2)
|
|
end
|
|
|
|
test "unhandled errors default to :error severity" do
|
|
@reporter.report(@error, handled: false)
|
|
assert_equal :error, @subscriber.events.dig(0, 2)
|
|
end
|
|
|
|
test "report errors only once" do
|
|
assert_difference -> { @subscriber.events.size }, +1 do
|
|
@reporter.report(@error, handled: false)
|
|
end
|
|
|
|
assert_no_difference -> { @subscriber.events.size } do
|
|
3.times do
|
|
@reporter.report(@error, handled: false)
|
|
end
|
|
end
|
|
end
|
|
|
|
test "can report frozen exceptions" do
|
|
assert_difference -> { @subscriber.events.size }, +1 do
|
|
@reporter.report(@error.freeze, handled: false)
|
|
end
|
|
end
|
|
|
|
class FailingErrorSubscriber
|
|
Error = Class.new(StandardError)
|
|
|
|
def initialize(error)
|
|
@error = error
|
|
end
|
|
|
|
def report(_error, handled:, severity:, context:, source:)
|
|
raise @error
|
|
end
|
|
end
|
|
|
|
test "subscriber errors are re-raised if no logger is set" do
|
|
subscriber_error = FailingErrorSubscriber::Error.new("Big Oopsie")
|
|
@reporter.subscribe(FailingErrorSubscriber.new(subscriber_error))
|
|
assert_raises FailingErrorSubscriber::Error do
|
|
@reporter.report(@error, handled: true)
|
|
end
|
|
end
|
|
|
|
test "subscriber errors are logged if a logger is set" do
|
|
subscriber_error = FailingErrorSubscriber::Error.new("Big Oopsie")
|
|
@reporter.subscribe(FailingErrorSubscriber.new(subscriber_error))
|
|
log = StringIO.new
|
|
@reporter.logger = ActiveSupport::Logger.new(log)
|
|
@reporter.report(@error, handled: true)
|
|
|
|
expected = "Error subscriber raised an error: Big Oopsie (ErrorReporterTest::FailingErrorSubscriber::Error)"
|
|
assert_equal expected, log.string.lines.first.chomp
|
|
end
|
|
end
|