diff --git a/NEWS.md b/NEWS.md index ea481644e94ec..108dfbc8fb2f8 100644 --- a/NEWS.md +++ b/NEWS.md @@ -92,6 +92,8 @@ Library improvements * Improve performance of `quantile` ([#14413]). + * The new `Base.StackTraces` module makes stack traces easier to use programmatically. ([#14469]) + Deprecated or removed --------------------- @@ -1771,3 +1773,4 @@ Too numerous to mention. [#14424]: https://github.com/JuliaLang/julia/issues/14424 [#14759]: https://github.com/JuliaLang/julia/issues/14759 [#14114]: https://github.com/JuliaLang/julia/issues/14114 +[#14469]: https://github.com/JuliaLang/julia/issues/14469 diff --git a/base/exports.jl b/base/exports.jl index 8e5f197cb47ae..49bce96a7e89c 100644 --- a/base/exports.jl +++ b/base/exports.jl @@ -9,6 +9,7 @@ export Pkg, Git, LibGit2, + StackTraces, Profile, Dates, Sys, @@ -1047,6 +1048,12 @@ export rethrow, systemerror, +# stack traces + StackTrace, + StackFrame, + stacktrace, + catch_stacktrace, + # types convert, fieldoffset, diff --git a/base/profile.jl b/base/profile.jl index 4703046e4ab86..19b68b742cea8 100644 --- a/base/profile.jl +++ b/base/profile.jl @@ -2,7 +2,7 @@ module Profile -import Base: hash, == +import Base.StackTraces: lookup, UNKNOWN export @profile @@ -117,7 +117,7 @@ end function getdict(data::Vector{UInt}) uip = unique(data) - Dict{UInt, LineInfo}([ip=>lookup(ip) for ip in uip]) + Dict{UInt, StackFrame}([ip=>lookup(ip) for ip in uip]) end # TODO update signature in docstring. @@ -166,35 +166,6 @@ Julia, and examine the resulting `*.mem` files. clear_malloc_data() = ccall(:jl_clear_malloc_data, Void, ()) -#### -#### Internal interface -#### -immutable LineInfo - func::ByteString - file::ByteString - line::Int - inlined_file::ByteString - inlined_line::Int - fromC::Bool - ip::Int64 # large enough that this struct can be losslessly read on any machine (32 or 64 bit) -end - -const UNKNOWN = LineInfo("?", "?", -1, "?", -1, true, 0) - -# -# If the LineInfo has function and line information, we consider two of them the same -# if they share the same function/line information. For unknown functions, line==ip -# so we never actually need to consider the .ip field. -# -==(a::LineInfo, b::LineInfo) = a.line == b.line && a.fromC == b.fromC && a.func == b.func && a.file == b.file - -function hash(li::LineInfo, h::UInt) - h += 0xf4fbda67fe20ce88 % UInt - h = hash(li.line, h) - h = hash(li.file, h) - h = hash(li.func, h) -end - # C wrappers start_timer() = ccall(:jl_profile_start_timer, Cint, ()) @@ -208,16 +179,6 @@ len_data() = convert(Int, ccall(:jl_profile_len_data, Csize_t, ())) maxlen_data() = convert(Int, ccall(:jl_profile_maxlen_data, Csize_t, ())) -function lookup(ip::Ptr{Void}) - info = ccall(:jl_lookup_code_address, Any, (Ptr{Void},Cint), ip, false) - if length(info) == 7 - return LineInfo(string(info[1]), string(info[2]), Int(info[3]), string(info[4]), Int(info[5]), info[6], Int64(info[7])) - else - return UNKNOWN - end -end -lookup(ip::UInt) = lookup(convert(Ptr{Void},ip)) - error_codes = Dict{Int,ASCIIString}( -1=>"cannot specify signal action for profiling", -2=>"cannot create the timer for profiling", @@ -282,7 +243,7 @@ function parse_flat(iplist, n, lidict, C::Bool) # The ones with no line number might appear multiple times in a single # backtrace, giving the wrong impression about the total number of backtraces. # Delete them too. - keep = !Bool[x == UNKNOWN || x.line == 0 || (x.fromC && !C) for x in lilist] + keep = !Bool[x == UNKNOWN || x.line == 0 || (x.from_c && !C) for x in lilist] n = n[keep] lilist = lilist[keep] lilist, n @@ -301,7 +262,7 @@ function flat{T<:Unsigned}(io::IO, data::Vector{T}, lidict::Dict, C::Bool, combi print_flat(io, lilist, n, combine, cols, sortedby) end -function print_flat(io::IO, lilist::Vector{LineInfo}, n::Vector{Int}, combine::Bool, cols::Integer, sortedby) +function print_flat(io::IO, lilist::Vector{StackFrame}, n::Vector{Int}, combine::Bool, cols::Integer, sortedby) p = liperm(lilist) lilist = lilist[p] n = n[p] @@ -330,8 +291,8 @@ function print_flat(io::IO, lilist::Vector{LineInfo}, n::Vector{Int}, combine::B maxfunc = 0 for li in lilist maxline = max(maxline, li.line) - maxfile = max(maxfile, length(li.file)) - maxfunc = max(maxfunc, length(li.func)) + maxfile = max(maxfile, length(string(li.file))) + maxfunc = max(maxfunc, length(string(li.func))) end wline = max(5, ndigits(maxline)) ntext = cols - wcounts - wline - 3 @@ -371,9 +332,9 @@ function tree_aggregate{T<:Unsigned}(data::Vector{T}) bt, counts end -tree_format_linewidth(x::LineInfo) = ndigits(x.line)+6 +tree_format_linewidth(x::StackFrame) = ndigits(x.line)+6 -function tree_format(lilist::Vector{LineInfo}, counts::Vector{Int}, level::Int, cols::Integer) +function tree_format(lilist::Vector{StackFrame}, counts::Vector{Int}, level::Int, cols::Integer) nindent = min(cols>>1, level) ndigcounts = ndigits(maximum(counts)) ndigline = maximum([tree_format_linewidth(x) for x in lilist]) @@ -394,18 +355,20 @@ function tree_format(lilist::Vector{LineInfo}, counts::Vector{Int}, level::Int, if showextra base = string(base, "+", nextra, " ") end - if li.line == li.ip + if li.line == li.pointer strs[i] = string(base, rpad(string(counts[i]), ndigcounts, " "), - " ","unknown function (ip: 0x",hex(li.ip,2*sizeof(Ptr{Void})), + " ", + "unknown function (pointer: 0x", + hex(li.pointer,2*sizeof(Ptr{Void})), ")") else base = string(base, rpad(string(counts[i]), ndigcounts, " "), " ", - truncto(string(li.file), widthfile), + truncto(li.file, widthfile), "; ", - truncto(string(li.func), widthfunc), + truncto(li.func, widthfunc), "; ") if li.line == -1 strs[i] = string(base, "(unknown line)") @@ -428,7 +391,7 @@ function tree{T<:Unsigned}(io::IO, bt::Vector{Vector{T}}, counts::Vector{Int}, l # Organize backtraces into groups that are identical up to this level if combine # Combine based on the line information - d = Dict{LineInfo,Vector{Int}}() + d = Dict{StackFrame,Vector{Int}}() for i = 1:length(bt) ip = bt[i][level+1] key = lidict[ip] @@ -441,7 +404,7 @@ function tree{T<:Unsigned}(io::IO, bt::Vector{Vector{T}}, counts::Vector{Int}, l end # Generate counts dlen = length(d) - lilist = Array(LineInfo, dlen) + lilist = Array(StackFrame, dlen) group = Array(Vector{Int}, dlen) n = Array(Int, dlen) i = 1 @@ -465,7 +428,7 @@ function tree{T<:Unsigned}(io::IO, bt::Vector{Vector{T}}, counts::Vector{Int}, l end # Generate counts, and do the code lookup dlen = length(d) - lilist = Array(LineInfo, dlen) + lilist = Array(StackFrame, dlen) group = Array(Vector{Int}, dlen) n = Array(Int, dlen) i = 1 @@ -516,7 +479,7 @@ function tree{T<:Unsigned}(io::IO, data::Vector{T}, lidict::Dict, C::Bool, combi end function callersf(matchfunc::Function, bt::Vector{UInt}, lidict) - counts = Dict{LineInfo, Int}() + counts = Dict{StackFrame, Int}() lastmatched = false for id in bt if id == 0 @@ -548,8 +511,10 @@ function truncto(str::ByteString, w::Int) ret end +truncto(str::Symbol, w::Int) = truncto(string(str), w) + # Order alphabetically (file, function) and then by line number -function liperm(lilist::Vector{LineInfo}) +function liperm(lilist::Vector{StackFrame}) comb = Array(ByteString, length(lilist)) for i = 1:length(lilist) li = lilist[i] @@ -568,7 +533,7 @@ warning_empty() = warn(""" Profile.init().""") function purgeC(data, lidict) - keep = Bool[d == 0 || lidict[d].fromC == false for d in data] + keep = Bool[d == 0 || lidict[d].from_c == false for d in data] data[keep] end diff --git a/base/stacktraces.jl b/base/stacktraces.jl new file mode 100644 index 0000000000000..7bcfc6b2ed5c4 --- /dev/null +++ b/base/stacktraces.jl @@ -0,0 +1,139 @@ +# This file is a part of Julia. License is MIT: http://julialang.org/license + +module StackTraces + + +import Base: hash, ==, show + +export StackTrace, StackFrame, stacktrace, catch_stacktrace + +""" + StackFrame + +Stack information representing execution context. +""" +immutable StackFrame + "the name of the function containing the execution context" + func::Symbol + "the path to the file containing the execution context" + file::Symbol + "the line number in the file containing the execution context" + line::Int + "the path to the file containing the context for inlined code" + inlined_file::Symbol + "the line number in the file containing the context for inlined code" + inlined_line::Int + "true if the code is from C" + from_c::Bool + "representation of the pointer to the execution context as returned by `backtrace`" + pointer::Int64 # Large enough to be read losslessly on 32- and 64-bit machines. +end + +StackFrame(func, file, line) = StackFrame(func, file, line, symbol(""), -1, false, 0) + +""" + StackTrace + +An alias for `Vector{StackFrame}` provided for convenience; returned by calls to +`stacktrace` and `catch_stacktrace`. +""" +typealias StackTrace Vector{StackFrame} + + +const UNKNOWN = StackFrame(symbol("???"), symbol("???"), 0, symbol(""), -1, true, 0) + + +#= +If the StackFrame has function and line information, we consider two of them the same if +they share the same function/line information. For unknown functions, line == pointer, so we +never actually need to consider the pointer field. +=# +function ==(a::StackFrame, b::StackFrame) + a.line == b.line && a.from_c == b.from_c && a.func == b.func && a.file == b.file +end + +function hash(frame::StackFrame, h::UInt) + h += 0xf4fbda67fe20ce88 % UInt + h = hash(frame.line, h) + h = hash(frame.file, h) + h = hash(frame.func, h) +end + + +""" + lookup(pointer::Union{Ptr{Void}, UInt}) -> StackFrame + +Given a pointer to an execution context (usually generated by a call to `backtrace`), looks +up stack frame context information. +""" +function lookup(pointer::Ptr{Void}) + return StackFrame(ccall(:jl_lookup_code_address, Any, (Ptr{Void}, Cint), pointer, 0)...) +end + +lookup(pointer::UInt) = lookup(convert(Ptr{Void}, pointer)) + +""" + stacktrace([trace::Vector{Ptr{Void}},] [c_funcs::Bool=false]) -> StackTrace + +Returns a stack trace in the form of a vector of `StackFrame`s. (By default stacktrace +doesn't return C functions, but this can be enabled.) When called without specifying a +trace, `stacktrace` first calls `backtrace`. +""" +function stacktrace(trace::Vector{Ptr{Void}}, c_funcs::Bool=false) + stack = map(lookup, trace)::StackTrace + + # Remove frames that come from C calls. + if !c_funcs + filter!(frame -> !frame.from_c, stack) + end + + # Remove frame for this function (and any functions called by this function). + remove_frames!(stack, :stacktrace) +end + +stacktrace(c_funcs::Bool=false) = stacktrace(backtrace(), c_funcs) + +""" + catch_stacktrace([c_funcs::Bool=false]) -> StackTrace + +Returns the stack trace for the most recent error thrown, rather than the current execution +context. +""" +catch_stacktrace(c_funcs::Bool=false) = stacktrace(catch_backtrace(), c_funcs) + +""" + remove_frames!(stack::StackTrace, name::Symbol) + +Takes a `StackTrace` (a vector of `StackFrames`) and a function name (a `Symbol`) and +removes the `StackFrame` specified by the function name from the `StackTrace` (also removing +all frames above the specified function). Primarily used to remove `StackTraces` functions +from the `StackTrace` prior to returning it. +""" +function remove_frames!(stack::StackTrace, name::Symbol) + splice!(stack, 1:findlast(frame -> frame.func == name, stack)) + return stack +end + +function remove_frames!(stack::StackTrace, names::Vector{Symbol}) + splice!(stack, 1:findlast(frame -> frame.func in names, stack)) + return stack +end + +function show(io::IO, frame::StackFrame; full_path::Bool=false) + file_info = "$(full_path ? frame.file : basename(string(frame.file))):$(frame.line)" + + if frame.inlined_file != Symbol("") + inline_info = string("[inlined code from ", file_info, "] ") + file_info = string( + full_path ? frame.inlined_file : basename(string(frame.inlined_file)), + ":", frame.inlined_line + ) + else + inline_info = "" + end + + print(io, string(inline_info, frame.func != "" ? frame.func : "?", " at ", file_info)) +end + + +end diff --git a/base/sysimg.jl b/base/sysimg.jl index 7a8126d6c9471..4bc75531d367e 100644 --- a/base/sysimg.jl +++ b/base/sysimg.jl @@ -273,6 +273,10 @@ include("libgit2.jl") include("pkg.jl") const Git = Pkg.Git +# Stack frames and traces +include("stacktraces.jl") +importall .StackTraces + # profiler include("profile.jl") importall .Profile diff --git a/doc/index.rst b/doc/index.rst index 7a62aeb794287..b1ef5c7781112 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -45,6 +45,7 @@ manual/embedding manual/packages manual/profile + manual/stacktraces manual/performance-tips manual/workflow-tips manual/style-guide @@ -79,6 +80,7 @@ stdlib/test stdlib/c stdlib/profile + stdlib/stacktraces .. _devdocs: diff --git a/doc/manual/index.rst b/doc/manual/index.rst index 960ee6f387d65..d601ce5606550 100644 --- a/doc/manual/index.rst +++ b/doc/manual/index.rst @@ -35,6 +35,7 @@ embedding packages profile + stacktraces performance-tips style-guide faq diff --git a/doc/manual/stacktraces.rst b/doc/manual/stacktraces.rst new file mode 100644 index 0000000000000..878b2a40af5ac --- /dev/null +++ b/doc/manual/stacktraces.rst @@ -0,0 +1,254 @@ +.. _man-stacktraces: + +.. currentmodule:: Base + +************ +Stack Traces +************ + +The :mod:`StackTraces` module provides simple stack traces that are both human readable and +easy to use programmatically. + +Viewing a stack trace +--------------------- + +The primary function used to obtain a stack trace is :func:`stacktrace`:: + + julia> stacktrace() + 3-element Array{StackFrame,1}: + eval at boot.jl:265 + [inlined code from REPL.jl:3] eval_user_input at REPL.jl:62 + [inlined code from REPL.jl:92] anonymous at task.jl:63 + +Calling :func:`stacktrace` returns a vector of :obj:`StackFrame` s. For ease of use, the +alias :obj:`StackTrace` can be used in place of ``Vector{StackFrame}``. (Examples with +``...`` indicate that output may vary depending on how the code is run.) + +.. doctest:: + + julia> example() = stacktrace() + example (generic function with 1 method) + + julia> example() + ...-element Array{StackFrame,1}: + example at none:1 + eval at boot.jl:265 + ... + + julia> @noinline child() = stacktrace() + child (generic function with 1 method) + + julia> @noinline parent() = child() + parent (generic function with 1 method) + + julia> grandparent() = parent() + grandparent (generic function with 1 method) + + julia> grandparent() + ...-element Array{StackFrame,1}: + child at none:1 + parent at none:1 + grandparent at none:1 + eval at boot.jl:265 + ... + +Note that when calling :func:`stacktrace` you'll typically see a frame with +``eval at boot.jl``. When calling :func:`stacktrace` from the REPL you'll also have a few +extra frames in the stack from ``REPL.jl``, usually looking something like this:: + + julia> example() = stacktrace() + example (generic function with 1 method) + + julia> example() + 4-element Array{StackFrame,1}: + example at none:1 + eval at boot.jl:265 + [inlined code from REPL.jl:3] eval_user_input at REPL.jl:62 + [inlined code from REPL.jl:92] anonymous at task.jl:63 + +Extracting useful information +----------------------------- + +Each :obj:`StackFrame` contains the function name, file name, line number, file and line +information for inlined functions, a flag indicating whether it is a C function (by default +C functions do not appear in the stack trace), and an integer representation of the pointer +returned by :func:`backtrace`: + +.. doctest:: + + julia> top_frame = stacktrace()[1] + eval at boot.jl:265 + + julia> top_frame.func + :eval + + julia> top_frame.file + symbol("./boot.jl") + + julia> top_frame.line + 265 + + julia> top_frame.inlined_file + symbol("") + + julia> top_frame.inlined_line + -1 + + julia> top_frame.from_c + false + +:: + + julia> top_frame.pointer + 13203085684 + +This makes stack trace information available programmatically for logging, error handling, +and more. + +Error handling +-------------- + +While having easy access to information about the current state of the callstack can be +helpful in many places, the most obvious application is in error handling and debugging. + +.. doctest:: + + julia> @noinline bad_function() = undeclared_variable + bad_function (generic function with 1 method) + + julia> @noinline example() = try + bad_function() + catch + stacktrace() + end + example (generic function with 1 method) + + julia> example() + ...-element Array{StackFrame,1}: + example at none:4 + eval at boot.jl:265 + ... + +You may notice that in the example above the first stack frame points points at line 4, +where :func:`stacktrace` is called, rather than line 2, where `bad_function` is called, and +``bad_function``'s frame is missing entirely. This is understandable, given that +:func:`stacktrace` is called from the context of the `catch`. While in this example it's +fairly easy to find the actual source of the error, in complex cases tracking down the +source of the error becomes nontrivial. + +This can be remedied by calling :func:`catch_stacktrace` instead of :func:`stacktrace`. +Instead of returning callstack information for the current context, :func:`catch_stacktrace` +returns stack information for the context of the most recent exception: + +.. doctest:: + + julia> @noinline bad_function() = undeclared_variable + bad_function (generic function with 1 method) + + julia> @noinline example() = try + bad_function() + catch + catch_stacktrace() + end + example (generic function with 1 method) + + julia> example() + ...-element Array{StackFrame,1}: + bad_function at none:1 + example at none:2 + eval at boot.jl:265 + ... + +Notice that the stack trace now indicates the appropriate line number and the missing frame. + +.. doctest:: + + julia> @noinline child() = error("Whoops!") + child (generic function with 1 method) + + julia> @noinline parent() = child() + parent (generic function with 1 method) + + julia> @noinline function grandparent() + try + parent() + catch err + println("ERROR: ", err.msg) + catch_stacktrace() + end + end + grandparent (generic function with 1 method) + + julia> grandparent() + ERROR: Whoops! + ...-element Array{StackFrame,1}: + child at none:1 + parent at none:1 + grandparent at none:3 + eval at boot.jl:265 + ... + +Comparison with :func:`backtrace` +--------------------------------- + +A call to :func:`backtrace` returns a vector of ``Ptr{Void}``, which may then be passed into +:func:`stacktrace` for translation:: + + julia> trace = backtrace() + 15-element Array{Ptr{Void},1}: + Ptr{Void} @0x000000010527895e + Ptr{Void} @0x0000000309bf6220 + Ptr{Void} @0x0000000309bf61a0 + Ptr{Void} @0x00000001052733b4 + Ptr{Void} @0x0000000105271a0e + Ptr{Void} @0x000000010527189d + Ptr{Void} @0x0000000105272e6d + Ptr{Void} @0x0000000105272cef + Ptr{Void} @0x0000000105285b88 + Ptr{Void} @0x000000010526b50e + Ptr{Void} @0x000000010663cc28 + Ptr{Void} @0x0000000309bbc20f + Ptr{Void} @0x0000000309bbbde7 + Ptr{Void} @0x0000000309bb0262 + Ptr{Void} @0x000000010527980e + + julia> stacktrace(trace) + 4-element Array{StackFrame,1}: + backtrace at error.jl:26 + eval at boot.jl:265 + [inlined code from REPL.jl:3] eval_user_input at REPL.jl:62 + [inlined code from REPL.jl:92] anonymous at task.jl:63 + +Notice that the vector returned by :func:`backtrace` had 15 pointers, while the vector returned by :func:`stacktrace` only has 3. This is because, by default, :func:`stacktrace` removes any lower-level C functions from the stack. If you want to include stack frames from C calls, you can do it like this:: + + julia> stacktrace(stack, true) + 15-element Array{StackFrame,1}: + [inlined code from task.c:651] rec_backtrace at task.c:711 + backtrace at error.jl:26 + jlcall_backtrace_23146 at :-1 + [inlined code from interpreter.c:55] jl_apply at interpreter.c:65 + eval at interpreter.c:214 + eval at interpreter.c:220 + eval_body at interpreter.c:601 + jl_toplevel_eval_body at interpreter.c:534 + jl_toplevel_eval_flex at toplevel.c:525 + jl_toplevel_eval_in_warn at builtins.c:590 + eval at boot.jl:265 + [inlined code from REPL.jl:3] eval_user_input at REPL.jl:62 + jlcall_eval_user_input_22658 at :-1 + [inlined code from REPL.jl:92] anonymous at task.jl:63 + [inlined code from julia.h:1352] jl_apply at task.c:246 + +Individual pointers returned by :func:`backtrace` can be translated into :obj:`StackFrame` s +by passing them into :func:`StackTraces.lookup`: + +.. doctest:: + + julia> pointer = backtrace()[1] + Ptr{Void} @0x... + + julia> frame = StackTraces.lookup(pointer) + [inlined code from task.c:663] rec_backtrace at task.c:723 + + julia> println("The top frame is from $(frame.func)!") + The top frame is from rec_backtrace! diff --git a/doc/stdlib/stacktraces.rst b/doc/stdlib/stacktraces.rst new file mode 100644 index 0000000000000..3a48958abae7f --- /dev/null +++ b/doc/stdlib/stacktraces.rst @@ -0,0 +1,68 @@ +.. module:: StackTraces + +.. _stdlib-stacktraces: + +************* + StackTraces +************* + +.. currentmodule:: Base + +.. data:: StackFrame + + Stack information representing execution context, with the following fields: + + ``func::Symbol`` + the name of the function containing the execution context + + ``file::Symbol`` + the path to the file containing the execution context + + ``line::Int`` + the line number in the file containing the execution context + + ``inlined_file::Symbol`` + the path to the file containing the context for inlined code + + ``inlined_line::Int`` + the line number in the file containing the context for inlined code + + ``from_c::Bool`` + true if the code is from C + + ``pointer::Int64`` + representation of the pointer to the execution context as returned by ``backtrace`` + +.. data:: StackTrace + + An alias for ``Vector{StackFrame}`` provided for convenience; returned by calls to + ``stacktrace`` and ``catch_stacktrace``. + +.. function:: stacktrace([trace::Vector{Ptr{Void}},] [c_funcs::Bool=false]) -> StackTrace + + .. Docstring generated from Julia source + + Returns a stack trace in the form of a vector of ``StackFrame``\ s. (By default stacktrace doesn't return C functions, but this can be enabled.) When called without specifying a trace, ``stacktrace`` first calls ``backtrace``\ . + +.. function:: catch_stacktrace([c_funcs::Bool=false]) -> StackTrace + + .. Docstring generated from Julia source + + Returns the stack trace for the most recent error thrown, rather than the current execution context. + +.. currentmodule:: Base.StackTraces + +The following methods and types in :mod:`Base.StackTraces` are not exported and need to be called e.g. as ``StackTraces.lookup(ptr)``. + +.. function:: lookup(pointer::Union{Ptr{Void}, UInt}) -> StackFrame + + .. Docstring generated from Julia source + + Given a pointer to an execution context (usually generated by a call to ``backtrace``\ ), looks up stack frame context information. + +.. function:: remove_frames!(stack::StackTrace, name::Symbol) + + .. Docstring generated from Julia source + + Takes a ``StackTrace`` (a vector of ``StackFrames``\ ) and a function name (a ``Symbol``\ ) and removes the ``StackFrame`` specified by the function name from the ``StackTrace`` (also removing all frames above the specified function). Primarily used to remove ``StackTraces`` functions from the ``StackTrace`` prior to returning it. + diff --git a/test/choosetests.jl b/test/choosetests.jl index aa6fd49205707..0b59ac73e70f9 100644 --- a/test/choosetests.jl +++ b/test/choosetests.jl @@ -29,8 +29,8 @@ function choosetests(choices = []) "combinatorics", "sysinfo", "rounding", "ranges", "mod2pi", "euler", "show", "lineedit", "replcompletions", "repl", "replutil", "sets", "test", "goto", "llvmcall", "grisu", - "nullable", "meta", "profile", "libgit2", "docs", "markdown", - "base64", "serialize", "functors", "misc", "threads", + "nullable", "meta", "stacktraces", "profile", "libgit2", "docs", + "markdown", "base64", "serialize", "functors", "misc", "threads", "enums", "cmdlineargs", "i18n", "workspace", "libdl", "int", "checked", "intset", "floatfuncs", "compile", "parallel", "inline", "boundscheck" diff --git a/test/meta.jl b/test/meta.jl index 51cf1239aa134..e1e076aec6cac 100644 --- a/test/meta.jl +++ b/test/meta.jl @@ -34,15 +34,15 @@ h_noinlined() = g_noinlined() function foundfunc(bt, funcname) for b in bt - lkup = Profile.lookup(b) + lkup = StackTraces.lookup(b) if lkup.func == funcname return true end end false end -@test !foundfunc(h_inlined(), "g_inlined") -@test foundfunc(h_noinlined(), "g_noinlined") +@test !foundfunc(h_inlined(), :g_inlined) +@test foundfunc(h_noinlined(), :g_noinlined) using Base.pushmeta!, Base.popmeta! diff --git a/test/stacktraces.jl b/test/stacktraces.jl new file mode 100644 index 0000000000000..f1a0c1d82ae2e --- /dev/null +++ b/test/stacktraces.jl @@ -0,0 +1,100 @@ +# This file is a part of Julia. License is MIT: http://julialang.org/license + +# Tests for /base/stacktraces.jl + +# Some tests don't currently work for Appveyor 32-bit Windows +const APPVEYOR_WIN32 = ( + OS_NAME == :Windows && WORD_SIZE == 32 && get(ENV, "APPVEYOR", "False") == "True" +) + +if !APPVEYOR_WIN32 + let + @noinline child() = stacktrace() + @noinline parent() = child() + @noinline grandparent() = parent() + line_numbers = @__LINE__ - [3, 2, 1] + + # Basic tests. + stack = grandparent() + @assert length(stack) >= 3 "Compiler has unexpectedly inlined functions" + @test [:child, :parent, :grandparent] == [f.func for f in stack[1:3]] + for (line, frame) in zip(line_numbers, stack[1:3]) + @test [Symbol(@__FILE__), line] in + ([frame.file, frame.line], [frame.inlined_file, frame.inlined_line]) + end + @test [false, false, false] == [f.from_c for f in stack[1:3]] + + # Test remove_frames! + stack = StackTraces.remove_frames!(grandparent(), :parent) + @test stack[1] == StackFrame(:grandparent, @__FILE__, line_numbers[3]) + + stack = StackTraces.remove_frames!(grandparent(), [:child, :something_nonexistent]) + @test stack[1:2] == [ + StackFrame(:parent, @__FILE__, line_numbers[2]), + StackFrame(:grandparent, @__FILE__, line_numbers[3]) + ] + end +end + +let + # Test from_c + default, with_c, without_c = stacktrace(), stacktrace(true), stacktrace(false) + @test default == without_c + @test length(with_c) > length(without_c) + @test !isempty(filter(frame -> frame.from_c, with_c)) + @test isempty(filter(frame -> frame.from_c, without_c)) +end + +@test StackTraces.lookup(C_NULL) == StackTraces.UNKNOWN + +let + # No errors should mean nothing in catch_backtrace + @test catch_backtrace() == StackFrame[] + + @noinline bad_function() = throw(UndefVarError(:nonexistent)) + function try_catch() + try + bad_function() + catch + return catch_stacktrace() + end + end + line_numbers = @__LINE__ - [8, 5] + + # Test try...catch with catch_stacktrace + @test try_catch()[1:2] == [ + StackFrame(:bad_function, @__FILE__, line_numbers[1]), + StackFrame(:try_catch, @__FILE__, line_numbers[2]) + ] +end + +let + # Test try...catch with stacktrace + function try_stacktrace() + try + error() + catch + true # noop corrects stacktrace line numbers + return stacktrace() + end + end + line_number = @__LINE__ - 3 + + @test try_stacktrace()[1] == StackFrame(:try_stacktrace, @__FILE__, line_number) + + # TODO: Demonstrates an issue that occurs when stacktraces is called at the beginning + # of a catch. Once the issue is corrected, this test case will fail and line_number + # should be adjusted to `@__LINE__ - 3` below. + function try_stacktrace_bad() + try + error() + true # Line reported by stacktraces + # Ignored + catch + return stacktrace() # Line that should be reported + end + end + line_number = @__LINE__ - 6 + + @test try_stacktrace_bad()[1] == StackFrame(:try_stacktrace_bad, @__FILE__, line_number) +end