diff --git a/.travis.yml b/.travis.yml index fcbf111..9327cf4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,7 +5,6 @@ os: - linux - osx julia: - - 0.7 - 1.0 - nightly notifications: @@ -13,14 +12,17 @@ notifications: git: depth: 99999999 -# TODO: remove this once HeaderREPLs is registered -before_script: - - julia -e 'using Pkg; - Pkg.clone("https://github.com/timholy/HeaderREPLs.jl")' - -after_script: # TODO: change to after_success once https://github.com/JuliaLang/julia/issues/28306 is fixed +after_success: # push coverage results to Codecov - julia -e 'using Pkg, Rebugger; cd(joinpath(dirname(pathof(Rebugger)), "..")); Pkg.add("Coverage"); using Coverage; Codecov.submit(Codecov.process_folder())' - # Update the documentation - - julia -e 'using Pkg; ps=Pkg.PackageSpec(name="Documenter", version="0.19"); Pkg.add(ps); Pkg.pin(ps)' - - julia -e 'using Rebugger; ENV["DOCUMENTER_DEBUG"] = "true"; include(joinpath(dirname(pathof(Rebugger)), "..", "docs", "make.jl"))' + +jobs: + include: + - stage: "Documentation" + julia: 1.0 + os: linux + script: + - julia --project=docs/ -e 'using Pkg; Pkg.develop(PackageSpec(path=pwd())); + Pkg.instantiate()' + - julia --project=docs/ docs/make.jl +after_success: skip diff --git a/README.md b/README.md index afb3fe4..858b7e2 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [![codecov.io](http://codecov.io/github/timholy/Rebugger.jl/coverage.svg?branch=master)](http://codecov.io/github/timholy/Rebugger.jl?branch=master) Rebugger is an expression-level debugger for Julia. -It has no ability to interact with or manipulate call stacks (see [ASTInterpreter2](https://github.com/Keno/ASTInterpreter2.jl)), +It has no ability to interact with or manipulate call stacks (see [Gallium](https://github.com/Keno/Gallium.jl)), but it can trace execution via the manipulation of Julia expressions. The name "Rebugger" has 3 meanings: @@ -19,7 +19,7 @@ The name "Rebugger" has 3 meanings: **See the documentation**: [![](https://img.shields.io/badge/docs-stable-blue.svg)](https://timholy.github.io/Rebugger.jl/stable) -[![](https://img.shields.io/badge/docs-latest-blue.svg)](https://timholy.github.io/Rebugger.jl/latest) +[![](https://img.shields.io/badge/docs-latest-blue.svg)](https://timholy.github.io/Rebugger.jl/dev) Note that Rebugger may benefit from custom configuration, as described in the documentation. diff --git a/REQUIRE b/REQUIRE index 88fe3a7..69c3ef0 100644 --- a/REQUIRE +++ b/REQUIRE @@ -1,3 +1,3 @@ -julia 0.7 -Revise 0.7.15 +julia 1.0 +Revise 1.0 HeaderREPLs 0.2 diff --git a/appveyor.yml b/appveyor.yml index f06875d..55c58b6 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,6 +1,5 @@ environment: matrix: - - julia_version: 0.7 - julia_version: 1 - julia_version: nightly diff --git a/docs/Project.toml b/docs/Project.toml new file mode 100644 index 0000000..ce87d15 --- /dev/null +++ b/docs/Project.toml @@ -0,0 +1,5 @@ +[deps] +Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" + +[compat] +Documenter = "~0.21" diff --git a/docs/make.jl b/docs/make.jl index 83cdb9b..c25915f 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -3,7 +3,7 @@ using Documenter, Rebugger makedocs( modules = [Rebugger], clean = false, - format = :html, + format = Documenter.HTML(prettyurls = get(ENV, "CI", nothing) == "true"), sitename = "Rebugger.jl", authors = "Tim Holy", linkcheck = !("skiplinks" in ARGS), @@ -15,15 +15,8 @@ makedocs( "internals.md", "reference.md", ], - # # Use clean URLs, unless built as a "local" build - # html_prettyurls = !("local" in ARGS), -# html_canonical = "https://juliadocs.github.io/Rebugger.jl/stable/", ) deploydocs( repo = "github.com/timholy/Rebugger.jl.git", - target = "build", - julia = "1.0", - deps = nothing, - make = nothing, ) diff --git a/docs/src/images/stepin4.png b/docs/src/images/stepin4.png new file mode 100644 index 0000000..9d107e2 Binary files /dev/null and b/docs/src/images/stepin4.png differ diff --git a/docs/src/index.md b/docs/src/index.md index f57ceab..6f9f4df 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -1,7 +1,7 @@ # Introduction to Rebugger Rebugger is an expression-level debugger for Julia. -It has no ability to interact with or manipulate call stacks (see [ASTInterpreter2](https://github.com/Keno/ASTInterpreter2.jl)), +It has no ability to interact with or manipulate call stacks (see [Gallium](https://github.com/Keno/Gallium.jl)), but it can trace execution via the manipulation of Julia expressions. The name "Rebugger" has 3 meanings: diff --git a/docs/src/usage.md b/docs/src/usage.md index 4297c27..04d652f 100644 --- a/docs/src/usage.md +++ b/docs/src/usage.md @@ -19,15 +19,21 @@ Select the expression you want to step into by positioning "point" (your cursor) at the desired location in the command line: ```@raw html - + ``` It's essential that point is at the very first character of the expression, in this case on the `s` in `show`. + +!!! note + Don't confuse the REPL's cursor with your mouse pointer. + Your mouse is essentially irrelevant on the REPL; use arrow keys or the other + [navigation features of Julia's REPL](https://docs.julialang.org/en/latest/stdlib/REPL/). + Now if you hit Meta-e, you should see something like this: ```@raw html - + ``` (If not, check [Keyboard shortcuts](@ref) and [Customize keybindings](@ref).) @@ -42,12 +48,12 @@ Indented blue line(s) show the value(s) of any input arguments or type parameter If you're following along, move your cursor to the next `show` call as illustrated above. Hit Meta-e again. You should see a new `show` method, this time with two input arguments. -Now let's demonstrate another important display item: position your cursor at the +Now let's demonstrate another important display item: position point at the beginning of the `_show_empty` call and hit Meta-e. The display should now look like this: ```@raw html - + ``` This time, note the yellow/orange line: this is a warning message, and you should pay attention to these. @@ -55,9 +61,23 @@ This time, note the yellow/orange line: this is a warning message, and you shoul In this case execution never reached `_show_empty`, because it enters `show_vector` instead; if you moved your cursor there, you could trace execution more completely. +You can edit these expressions to insert code to display variables or test +changes to the code. +As an experiment, try stepping into the `show_vector` call from the example above +and adding `@show limited` to display a local variable's value: + +```@raw html + +``` + +!!! note + When editing expressions, you can insert a blank line with Meta-Enter (i.e., Esc-Enter, Alt-Enter, or Option-Enter). + See the many [advanced features of Julia's REPL](https://docs.julialang.org/en/latest/stdlib/REPL/#Key-bindings-1) that allow you to efficiently edit these `let`-blocks. + Having illustrated the importance of "point" and the various colors used for messages from Rebugger, to ensure readability the remaining examples will be rendered as text. + ## Capturing stacktraces For a quick demo, we'll use the `Colors` package (`add` it if you don't have it) @@ -81,19 +101,21 @@ in expression starting at REPL[3]:1 ``` To capture the stacktrace, type the last line again or hit the up arrow, but instead of -pressing enter type Meta-s. +pressing Enter, type Meta-s. After a short delay, you should see something like this: + ```julia julia> colorant"hsl(80%, 20%, 15%)" ┌ Warning: Tuple{getfield(Colors, Symbol("#@colorant_str")),LineNumberNode,Module,Any} was not found, perhaps it was generated by code -└ @ Revise ~/.julia/dev/Revise/src/Revise.jl:614 +└ @ Revise ~/.julia/dev/Revise/src/Revise.jl:659 Captured elements of stacktrace: -[1] parse_hsl_hue(num::AbstractString) in Colors at /home/tim/.julia/dev/Colors/src/parse.jl:25 -[2] _parse_colorant(desc::AbstractString) in Colors at /home/tim/.julia/dev/Colors/src/parse.jl:51 -[3] parse(::Type{C}, desc::AbstractString) where C<:Colorant in Colors at /home/tim/.julia/dev/Colors/src/parse.jl:140 -parse_hsl_hue(num::AbstractString) in Colors at /home/tim/.julia/dev/Colors/src/parse.jl:25 +[1] parse_hsl_hue(num::AbstractString) in Colors at /home/tim/.julia/packages/Colors/4hvzi/src/parse.jl:25 +[2] _parse_colorant(desc::AbstractString) in Colors at /home/tim/.julia/packages/Colors/4hvzi/src/parse.jl:51 +[3] _parse_colorant(::Type{C}, ::Type{SUP}, desc::AbstractString) where {C<:Colorant, SUP} in Colors at /home/tim/.julia/packages/Colors/4hvzi/src/parse.jl:112 +[4] parse(::Type{C}, desc::AbstractString) where C<:Colorant in Colors at /home/tim/.julia/packages/Colors/4hvzi/src/parse.jl:140 +parse_hsl_hue(num::AbstractString) in Colors at /home/tim/.julia/packages/Colors/4hvzi/src/parse.jl:25 num = 80% -rebug> @eval Colors let (num,) = Main.Rebugger.getstored("c592f0a4-a226-11e8-1002-fd2731558606") +rebug> @eval Colors let (num,) = Main.Rebugger.getstored("57dbc76a-0def-11e9-1dbf-ef97d29d2e25") begin if num[end] == '%' error("hue cannot end in %") @@ -105,13 +127,41 @@ rebug> @eval Colors let (num,) = Main.Rebugger.getstored("c592f0a4-a226-11e8-100 ``` (Again, if this doesn't happen check [Keyboard shortcuts](@ref) and [Customize keybindings](@ref).) +You are in the method corresponding to `[1]` in the stacktrace. Now you can navigate with your up and down arrows to browse the captured stacktrace. -You can pick any of these expressions to execute (hit Enter) or edit before execution. -For example you could add `@show` commands to examine intermediate variables or test -out different ways to fix a bug. +For example, if you hit the up arrow twice, you will be in the method corresponding to `[3]`: + +```julia +julia> colorant"hsl(80%, 20%, 15%)" +┌ Warning: Tuple{getfield(Colors, Symbol("#@colorant_str")),LineNumberNode,Module,Any} was not found, perhaps it was generated by code +└ @ Revise ~/.julia/dev/Revise/src/Revise.jl:659 +Captured elements of stacktrace: +[1] parse_hsl_hue(num::AbstractString) in Colors at /home/tim/.julia/packages/Colors/4hvzi/src/parse.jl:25 +[2] _parse_colorant(desc::AbstractString) in Colors at /home/tim/.julia/packages/Colors/4hvzi/src/parse.jl:51 +[3] _parse_colorant(::Type{C}, ::Type{SUP}, desc::AbstractString) where {C<:Colorant, SUP} in Colors at /home/tim/.julia/packages/Colors/4hvzi/src/parse.jl:112 +[4] parse(::Type{C}, desc::AbstractString) where C<:Colorant in Colors at /home/tim/.julia/packages/Colors/4hvzi/src/parse.jl:140 +_parse_colorant(::Type{C}, ::Type{SUP}, desc::AbstractString) where {C<:Colorant, SUP} in Colors at /home/tim/.julia/packages/Colors/4hvzi/src/parse.jl:112 + C = Colorant + SUP = Any + desc = hsl(80%, 20%, 15%) +rebug> @eval Colors let (C, SUP, desc) = Main.Rebugger.getstored("57d9ebc0-0def-11e9-2ab0-e5d1e4c6e82d") + begin + _parse_colorant(desc) + end + end +``` + +You can hit the down arrow and go back to earlier entries in the trace. +Alternatively, you can pick any of these expressions to execute (hit Enter) or edit before execution. You can use the REPL history to test the results of many different changes to the same "method"; the "method" will be run with the same inputs each time. +!!! note + When point is at the end of the input, the up and down arrows step through the history. + But if you move point into the method body (e.g., by using left-arrow), + the up and down arrows move within the method body. + If you've entered edit mode, you can go back to history mode using PgUp and PgDn. + ## Important notes ### "Missing" methods from stacktraces @@ -119,9 +169,6 @@ the "method" will be run with the same inputs each time. In the example above, you may have noticed the warning about the `@colorant_str` macro being omitted from the "captured" (interactive) expressions comprising the stacktrace. Macros are not traced. -Also notice that the inlined method does not appear in the captured stacktrace. -However, you can enter an inlined method using "step in," starting from the method -above it in the stacktrace. When many methods use keyword arguments, the apparent difference between the "real" stacktrace and the "captured" stacktrace can be quite dramatic: @@ -154,29 +201,21 @@ Stacktrace: julia> Pkg.add("NoPkg") # hit Meta-s here Captured elements of stacktrace: [1] pkgerror(msg::String...) in Pkg.Types at /home/tim/src/julia-1.0/usr/share/julia/stdlib/v1.0/Pkg/src/Types.jl:120 -[2] ensure_resolved(env::Pkg.Types.EnvCache, pkgs::AbstractArray{Pkg.Types.PackageSpec,1}) in Pkg.Types at /home/tim/src/julia-1.0/usr/share/julia/stdlib/v1.0/Pkg/src/Types.jl:860 -[3] add_or_develop(ctx::Pkg.Types.Context, pkgs::Array{Pkg.Types.PackageSpec,1}) in Pkg.API at /home/tim/src/julia-1.0/usr/share/julia/stdlib/v1.0/Pkg/src/API.jl:32 -[4] add_or_develop(pkgs::Array{String,1}) in Pkg.API at /home/tim/src/julia-1.0/usr/share/julia/stdlib/v1.0/Pkg/src/API.jl:28 -[5] add(args...) in Pkg.API at /home/tim/src/julia-1.0/usr/share/julia/stdlib/v1.0/Pkg/src/API.jl:69 +[2] add(args...) in Pkg.API at /home/tim/src/julia-1.0/usr/share/julia/stdlib/v1.0/Pkg/src/API.jl:69 pkgerror(msg::String...) in Pkg.Types at /home/tim/src/julia-1.0/usr/share/julia/stdlib/v1.0/Pkg/src/Types.jl:120 msg = ("The following package names could not be resolved:\n * NoPkg (not found in project, manifest or registry)\nPlease specify by known `name=uuid`.",) -rebug> @eval Pkg.Types let (msg,) = Main.Rebugger.getstored("b5c899c2-a228-11e8-0877-d102334a9f65") +rebug> @eval Pkg.Types let (msg,) = Main.Rebugger.getstored("161c53ba-0dfe-11e9-0f8f-59f468aec692") begin throw(PkgError(join(msg))) end end ``` -Note that only five methods got captured but the stacktrace is much longer. +Note that only two methods got captured but the stacktrace is much longer. Most of these methods, however, start with `#`, an indication that they are -generated methods rather than ones that appear in the source code. -The interactive stacktrace visits only those methods that appear in the original source code. - -!!! note - `Pkg` is one of Julia's standard libraries, and to step into or trace Julia's stdlibs - you must build Julia from source. - - +generated (keyword-handling) methods rather than ones that appear directly in the source code. +For now, Rebugger omits these entries. +However, you can enter (i.e., Meta-e) such methods from one that is higher in the stack trace. ### Modified "signatures" diff --git a/src/Rebugger.jl b/src/Rebugger.jl index ce78465..0e0ddc1 100644 --- a/src/Rebugger.jl +++ b/src/Rebugger.jl @@ -23,21 +23,27 @@ function repl_init(repl) repl.interface = REPL.setup_interface(repl; extra_repl_keymap = get_rebugger_modeswitch_dict()) end -function __init__() +function rebugrepl_init() # Set up the Rebugger REPL mode with all of its key bindings repl_inited = isdefined(Base, :active_repl) - @async begin - while !isdefined(Base, :active_repl) - sleep(0.05) - end - sleep(0.1) # for extra safety - # Set up the custom "rebug" REPL - main_repl = Base.active_repl - repl = HeaderREPL(main_repl, RebugHeader()) - interface = REPL.setup_interface(repl; extra_repl_keymap=[get_rebugger_modeswitch_dict(), rebugger_keys]) - rebug_prompt_ref[] = interface.modes[end] - add_keybindings(; override=repl_inited, deprecated_keybindings..., keybindings...) + while !isdefined(Base, :active_repl) + sleep(0.05) end + sleep(0.1) # for extra safety + # Set up the custom "rebug" REPL + main_repl = Base.active_repl + repl = HeaderREPL(main_repl, RebugHeader()) + interface = REPL.setup_interface(repl; extra_repl_keymap=[get_rebugger_modeswitch_dict(), rebugger_keys]) + rebug_prompt_ref[] = interface.modes[end] + add_keybindings(; override=repl_inited, deprecated_keybindings..., keybindings...) +end + + +function __init__() + schedule(Task(rebugrepl_init)) end +include("precompile.jl") +_precompile_() + end # module diff --git a/src/debug.jl b/src/debug.jl index 7a2d013..f1053ad 100644 --- a/src/debug.jl +++ b/src/debug.jl @@ -35,6 +35,7 @@ struct EvalException <: Exception exception end +const base_prefix = '.' * Base.Filesystem.path_separator """ Rebugger.clear() @@ -89,31 +90,15 @@ function pregenerated_stacktrace(trace; topname = :capture_stacktrace) usrtrace, defs = Method[], RelocatableExpr[] methodsused = Set{Method}() - function load_file(file, mod=Base) - ret = Revise.find_file(file, mod) - ret == nothing && return nothing - file, recipemod = ret - if !haskey(Revise.fileinfos, file) - try - @info "tracking $recipemod" - Revise.track(recipemod) - catch err - err isa Revise.GitRepoException && return nothing - rethrow(err) - end - end - return file - end - # When the method can't be found directly in the tables, # look it up by fie and line number - function add_by_file_line(defmap, sf) + function add_by_file_line(defmap, line) for (def, info) in defmap info == nothing && continue sigts, offset = info r = linerange(def, offset) r == nothing && continue - if sf.line ∈ r + if line ∈ r mths = Base._methods_by_ftype(last(sigts), -1, typemax(UInt)) m = mths[end][3] # the last method is the least specific that matches the signature (which would be more specific if it were used) if m ∉ methodsused @@ -126,6 +111,16 @@ function pregenerated_stacktrace(trace; topname = :capture_stacktrace) end return false end + function add_by_file_line(pkgdata, file, line) + fi = get(pkgdata.fileinfos, file, nothing) + if fi !== nothing + Revise.maybe_parse_from_cache!(pkgdata, file) + for (mod, fmm) in fi.fm + add_by_file_line(fmm.defmap, line) && return true + end + end + return false + end for (i, sf) in enumerate(trace) sf.func == topname && break # truncate at the chosen spot @@ -134,36 +129,65 @@ function pregenerated_stacktrace(trace; topname = :capture_stacktrace) file = String(sf.file) if mi isa Core.MethodInstance method = mi.def - # Set up tracking, if necessary - if !haskey(Revise.fileinfos, file) - file = load_file(file, method.module) + def = nothing + if String(method.name)[1] != '#' # if not a keyword/default arg method + try + def = Revise.get_def(method) + catch + continue + end end - haskey(Revise.fileinfos, file) || continue - fi = Revise.fileinfos[file] - Revise.maybe_parse_from_cache!(fi, file) - funcname = String(sf.func) - if startswith(funcname, '#') - # This is a generated method, perhaps it's a keyword function handler + if def === nothing + # This may be a generated method, perhaps it's a keyword function handler # Look for it by line number - defmap = fi.fm[method.module].defmap - add_by_file_line(defmap, sf) + local id + try + id = Revise.get_tracked_id(method.module) + catch + # Methods from Core.Compiler cause errors on Julia binaries + continue + end + id === nothing && continue + pkgdata = Revise.pkgdatas[id] + cfile = get(Revise.src_file_key, file, file) + rpath = relpath(cfile, pkgdata) + haskey(pkgdata.fileinfos, rpath) || continue + Revise.maybe_parse_from_cache!(pkgdata, rpath) + fi = get(pkgdata.fileinfos, rpath, nothing) + if fi !== nothing + add_by_file_line(fi.fm[method.module].defmap, sf) + end else method ∈ methodsused && continue - def = Revise.get_def(method; modified_files=String[]) def isa ExLike || continue push!(defs, def) push!(usrtrace, method) end else # This method was inlined and hence linfo was not available - if !haskey(Revise.fileinfos, file) - file = load_file(file) + # Try to find it + if startswith(file, base_prefix) + # This is a file in Base or Core + file = relpath(file, base_prefix) + id = Revise.get_tracked_id(Base) + pkgdata = Revise.pkgdatas[id] + if haskey(pkgdata.fileinfos, file) + add_by_file_line(pkgdata, file, sf.line) && continue + elseif startswith(file, "compiler") + try + id = Revise.get_tracked_id(Core.Compiler) + catch + # On Julia binaries Core.Compiler is not available + continue + end + pkgdata = Revise.pkgdatas[id] + add_by_file_line(pkgdata, relpath(file, pkgdata), sf.line) && continue + end end - haskey(Revise.fileinfos, file) || continue - fi = Revise.fileinfos[file] - Revise.maybe_parse_from_cache!(fi, file) - for (mod, fmm) in fi.fm - add_by_file_line(fmm.defmap, sf) && break + # Try all loaded packages + for (id, pkgdata) in Revise.pkgdatas + rpath = relpath(file, pkgdata) + add_by_file_line(pkgdata, rpath, sf.line) && break end end end @@ -449,7 +473,7 @@ function method_capture_from_callee(method; kwargs...) # Could use a default arg above but this generates a more understandable error message local def try - def = get_def(method; modified_files=String[]) + def = get_def(method; modified_files=typeof(Revise.revision_queue)()) catch err throw(DefMissing(method, err)) end diff --git a/src/precompile.jl b/src/precompile.jl new file mode 100644 index 0000000..a2a1da1 --- /dev/null +++ b/src/precompile.jl @@ -0,0 +1,5 @@ +function _precompile_() + ccall(:jl_generating_output, Cint, ()) == 1 || return nothing + precompile(Tuple{typeof(get_rebugger_modeswitch_dict)}) + precompile(Tuple{typeof(rebugrepl_init)}) +end diff --git a/test/runtests.jl b/test/runtests.jl index cf2bd6e..1cdb65d 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,6 +1,6 @@ using Rebugger using Rebugger: StopException -using Test, UUIDs, InteractiveUtils, REPL, HeaderREPLs +using Test, UUIDs, InteractiveUtils, REPL, Pkg, HeaderREPLs using REPL.LineEdit using Revise, Colors @@ -315,7 +315,8 @@ Base.show(io::IO, ::ErrorsOnShow) = throw(ArgumentError("no show")) # A case that tests inlining and several other aspects of argument capture ex = :([1, 2, 3] .* [1, 2]) - # Capture the actual stack trace, trimming it to avoid anything involving the `eval` itself + # Capture the actual stack trace, trimming it to avoid + # anything involving the `eval` itself trace = try Core.eval(Main, ex) catch @@ -339,6 +340,15 @@ Base.show(io::IO, ::ErrorsOnShow) = throw(ArgumentError("no show")) for (uuid, t) in zip(reverse(uuids), trace) @test Rebugger.stored[uuid].method.name == t.func end + + # Try capturing a method from Core. On binaries this would throw + # if we didn't catch it. + # Because the first entry is "top-level scope", and that terminates + # processing in Rebugger.pregenerated_stacktrace, we have to intervene a bit. + mod, ex = Main, :(Core.throw(ArgumentError("oops"))) + trace = try Core.eval(mod, command) catch err stacktrace(catch_backtrace()) end + usrtrace, defs = Rebugger.pregenerated_stacktrace(trace[2:3]) + @test usrtrace isa Vector end end @@ -429,6 +439,14 @@ Base.show(io::IO, ::ErrorsOnShow) = throw(ArgumentError("no show")) @test occursin("error", hist.history[idx[end]]) end + @testset "Pkg demo" begin + updated = Pkg.UPDATED_REGISTRY_THIS_SESSION[] + Pkg.UPDATED_REGISTRY_THIS_SESSION[] = true + uuids = Rebugger.capture_stacktrace(Pkg, :(add("NoPkg"))) + @test length(uuids) >= 2 + Pkg.UPDATED_REGISTRY_THIS_SESSION[] = updated + end + @testset "Empty stacktraces" begin cmd = "ccall(:jl_throw, Nothing, (Any,), ArgumentError(\"oops\"))" mktemp() do path, io