Last active
September 7, 2017 13:12
-
-
Save stanio/13d74294ca1868fed7fb to your computer and use it in GitHub Desktop.
Formatting function to provide full exception chain backtrace
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# frozen_string_literal: true | |
# | |
# This module, both source code and documentation, | |
# is in the Public Domain, and comes with NO WARRANTY. | |
# | |
require 'stringio' | |
module ExceptionFormatter | |
module_function | |
# | |
# Employs the Ruby 2.1 <code>Exception#cause</code> to provide backtrace of | |
# the complete exception chain. | |
# | |
# Given the following test: | |
# | |
# require_relative 'exception_formatter' | |
# include ExceptionFormatter | |
# | |
# def lower_level; raise 'The ultimate lower cause' end | |
# def level_one; lower_level rescue raise 'Exception reported uniformly for level 1' end | |
# def level_two; level_one end # Exception propagated from level 2 | |
# def level_three; level_two rescue raise 'Exception reported uniformly for level 3' end | |
# | |
# begin | |
# level_three | |
# rescue => e | |
# puts format_backtrace(e) | |
# end | |
# | |
# the following output is produced: | |
# | |
# RuntimeError: Exception reported uniformly for level 3 | |
# test.rb:7:in `rescue in level_three' | |
# test.rb:7:in `level_three' | |
# test.rb:10:in `<main>' | |
# Caused by: RuntimeError: Exception reported uniformly for level 1 | |
# test.rb:5:in `rescue in level_one' | |
# test.rb:5:in `level_one' | |
# test.rb:6:in `level_two' | |
# ... 2 more | |
# Caused by: RuntimeError: The ultimate lower cause | |
# test.rb:4:in `lower_level' | |
# ... 4 more | |
# | |
# Changing the test like: | |
# | |
# require_relative 'exception_formatter' | |
# include ExceptionFormatter | |
# | |
# def lower_level; raise 'The ultimate lower cause' end | |
# def level_one; lower_level end # Exception propagated from level 1 | |
# def level_two; level_one rescue raise 'Exception reported uniformly for level 2' end | |
# def level_three; level_two rescue raise 'Exception reported uniformly for level 3' end | |
# | |
# begin | |
# level_three | |
# rescue => e | |
# format_backtrace(e, $stderr) | |
# end | |
# | |
# produces: | |
# | |
# RuntimeError: Exception reported uniformly for level 3 | |
# test2.rb:7:in `rescue in level_three' | |
# test2.rb:7:in `level_three' | |
# test2.rb:10:in `<main>' | |
# Caused by: RuntimeError: Exception reported uniformly for level 2 | |
# test2.rb:6:in `rescue in level_two' | |
# test2.rb:6:in `level_two' | |
# ... 2 more | |
# Caused by: RuntimeError: The ultimate lower cause | |
# test2.rb:4:in `lower_level' | |
# test2.rb:5:in `level_one' | |
# ... 3 more | |
# | |
# That is, only the top-level exception trace is shown in full, and traces | |
# of nested exceptions is filtered to show just their unique parts (not | |
# repeating redundant stacks). | |
# | |
# @param exception [Exception] exception to format | |
# @param io [IO] output stream to write the result to. If not given the | |
# result is returned as a String | |
# @return [String] if no output stream given – formatted backtrace. | |
# @return [Void] if output stream given for the result. | |
# @see Exception#cause | |
# | |
def format_backtrace(exception, io = nil) | |
out = io || StringIO.new | |
_format_single(out, exception) | |
current_backtrace = exception.backtrace | |
cause = exception.cause | |
until cause.nil? do | |
cause_trace = _unique_trace(cause.backtrace.to_a, current_backtrace) | |
out.print 'Caused by: ' | |
_format_single(out, cause, cause_trace) | |
backtrace_length = cause.backtrace.length | |
out.puts "\t... #{backtrace_length - cause_trace.length} more" \ | |
if backtrace_length > cause_trace.length | |
current_backtrace = cause.backtrace | |
cause = cause.cause | |
end | |
out.string if io.nil? | |
end | |
def _format_single(out, exception, backtrace=nil) | |
out.puts "#{exception.class}: #{exception.message}" | |
backtrace ||= exception.backtrace.to_a | |
backtrace.each { |trace| out.puts "\t#{trace}" } | |
end | |
private_class_method :_format_single | |
def _unique_trace(backtrace1, backtrace2) | |
i = 1 | |
while i <= backtrace1.size && i <= backtrace2.size | |
break if backtrace1[-i] != backtrace2[-i] | |
i += 1 | |
end | |
backtrace1[0..-i] | |
end | |
private_class_method :_unique_trace | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
require_relative 'exception_formatter' | |
include ExceptionFormatter | |
def lower_level; raise 'The ultimate lower cause' end | |
def level_one; lower_level rescue raise 'Exception reported uniformly for level 1' end | |
def level_two; level_one end # Exception propagated from level 2 | |
def level_three; level_two rescue raise 'Exception reported uniformly for level 3' end | |
begin | |
level_three | |
rescue => e | |
puts format_backtrace(e) | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
require_relative 'exception_formatter' | |
include ExceptionFormatter | |
def lower_level; raise 'The ultimate lower cause' end | |
def level_one; lower_level end # Exception propagated from level 1 | |
def level_two; level_one rescue raise 'Exception reported uniformly for level 2' end | |
def level_three; level_two rescue raise 'Exception reported uniformly for level 3' end | |
begin | |
level_three | |
rescue => e | |
format_backtrace(e, $stderr) | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment