Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Nicer output when an exception bubbles through main #4968

Merged
merged 12 commits into from
Sep 14, 2017
Merged
11 changes: 8 additions & 3 deletions spec/std/callstack_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ describe "Backtrace" do
tempfile.close
sample = "#{__DIR__}/data/backtrace_sample"

# CallStack tries to make files relative to the current dir,
# so we do the same for tests
current_dir = Dir.current
current_dir += File::SEPARATOR unless current_dir.ends_with?(File::SEPARATOR)
sample = sample.lchop(current_dir)

`bin/crystal build --debug #{sample.inspect} -o #{tempfile.path.inspect}`
File.exists?(tempfile.path).should be_true

Expand All @@ -17,9 +23,8 @@ describe "Backtrace" do
output = `#{tempfile.path}`

# resolved file line:column
output.should match(/callee1 at #{sample} 3:10/)
output.should match(/callee3 at #{sample} 15:3/)
output.should match(/__crystal_main at #{sample} 17:1/)
output.should match(/#{sample}:3:10 in 'callee1'/)
output.should match(/#{sample}:15:3 in 'callee3'/)

# skipped internal details
output.should_not match(/src\/callstack\.cr/)
Expand Down
54 changes: 49 additions & 5 deletions src/callstack.cr
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@ end

# :nodoc:
struct CallStack
# Compute current directory at the beginning so filenames
# are always shown relative to the *starting* working directory.
CURRENT_DIR = begin
dir = Process::INITIAL_PWD
dir += File::SEPARATOR unless dir.ends_with?(File::SEPARATOR)
dir
end

@@skip = [] of String

def self.skip(filename)
Expand Down Expand Up @@ -145,27 +153,63 @@ struct CallStack
end

private def decode_backtrace
show_full_info = ENV["CRYSTAL_CALLSTACK_FULL_INFO"]? == "1"

@callstack.compact_map do |ip|
pc = CallStack.decode_address(ip)

file, line, column = CallStack.decode_line_number(pc)
if file == "??"
file_line_column = "??"
else

if file && file != "??"
next if @@skip.includes?(file)
file_line_column = "#{file} #{line}:#{column}"

# Turn to relative to the current dir, if possible
file = file.lchop(CURRENT_DIR)

file_line_column = "#{file}:#{line}:#{column}"
end

if name = CallStack.decode_function_name(pc)
function = name
elsif frame = CallStack.decode_frame(ip)
_, sname = frame
function = String.new(sname)

# We ignore these because they are part of `raise`'s internals,
# and we can't rely on a correct filename being available
# (for example if on Mac and without running `dsymutil`)
#
# We also ignore `main` because it's always at the same place
# and adds no info.
if function.starts_with?("*raise<") ||
function.starts_with?("*CallStack::") ||
function.starts_with?("*CallStack#")
next
end

# Crystal methods (their mangled name) start with `*`, so
# we remove that to have less clutter in the output.
function = function.lchop('*')
else
function = "???"
end

"0x#{ip.address.to_s(16)}: #{function} at #{file_line_column}"
if file_line_column
if show_full_info && (frame = CallStack.decode_frame(ip))
_, sname = frame
line = "#{file_line_column} in '#{String.new(sname)}'"
else
line = "#{file_line_column} in '#{function}'"
end
else
line = function
end

if show_full_info
line = "#{line} at 0x#{ip.address.to_s(16)}"
end

line
end
end

Expand Down
1 change: 1 addition & 0 deletions src/exception.cr
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ class Exception
def inspect_with_backtrace(io : IO)
io << message << " (" << self.class << ")\n"
backtrace?.try &.each do |frame|
io.print " from "
io.puts frame
end
io.flush
Expand Down