Skip to content

Commit c1b84fa

Browse files
authored
Merge pull request #52 from timholy/teh/stacktrace
Better handling of stacktraces
2 parents 0d08070 + 7c1b990 commit c1b84fa

File tree

4 files changed

+106
-35
lines changed

4 files changed

+106
-35
lines changed

REQUIRE

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
julia 0.7
2-
Revise 0.7.3
2+
Revise 0.7.15
33
HeaderREPLs 0.2

src/debug.jl

+54-31
Original file line numberDiff line numberDiff line change
@@ -88,27 +88,55 @@ and so on.
8888
function pregenerated_stacktrace(trace; topname = :capture_stacktrace)
8989
usrtrace, defs = Method[], RelocatableExpr[]
9090
methodsused = Set{Method}()
91+
92+
function load_file(file, mod=Base)
93+
ret = Revise.find_file(file, mod)
94+
ret == nothing && return nothing
95+
file, recipemod = ret
96+
if !haskey(Revise.fileinfos, file)
97+
try
98+
@info "tracking $recipemod"
99+
Revise.track(recipemod)
100+
catch err
101+
err isa Revise.GitRepoException && return nothing
102+
rethrow(err)
103+
end
104+
end
105+
return file
106+
end
107+
108+
# When the method can't be found directly in the tables,
109+
# look it up by fie and line number
110+
function add_by_file_line(defmap, sf)
111+
for (def, info) in defmap
112+
info == nothing && continue
113+
sigts, offset = info
114+
r = linerange(def, offset)
115+
r == nothing && continue
116+
if sf.line r
117+
mths = Base._methods_by_ftype(last(sigts), -1, typemax(UInt))
118+
m = mths[end][3] # the last method is the least specific that matches the signature (which would be more specific if it were used)
119+
if m methodsused
120+
push!(defs, def)
121+
push!(usrtrace, m)
122+
push!(methodsused, m)
123+
return true
124+
end
125+
end
126+
end
127+
return false
128+
end
129+
91130
for (i, sf) in enumerate(trace)
92131
sf.func == topname && break # truncate at the chosen spot
93132
sf.func notrace && continue
94133
mi = sf.linfo
134+
file = String(sf.file)
95135
if mi isa Core.MethodInstance
96136
method = mi.def
97137
# Set up tracking, if necessary
98-
file = String(sf.file)
99138
if !haskey(Revise.fileinfos, file)
100-
ret = Revise.find_file(file, method.module)
101-
ret == nothing && continue
102-
file, recipemod = ret
103-
if !haskey(Revise.fileinfos, file)
104-
try
105-
@info "tracking $recipemod"
106-
Revise.track(recipemod)
107-
catch err
108-
err isa Revise.GitRepoException && continue
109-
rethrow(err)
110-
end
111-
end
139+
file = load_file(file, method.module)
112140
end
113141
haskey(Revise.fileinfos, file) || continue
114142
fi = Revise.fileinfos[file]
@@ -118,31 +146,25 @@ function pregenerated_stacktrace(trace; topname = :capture_stacktrace)
118146
# This is a generated method, perhaps it's a keyword function handler
119147
# Look for it by line number
120148
defmap = fi.fm[method.module].defmap
121-
for (def, info) in defmap
122-
info == nothing && continue
123-
sigts, offset = info
124-
r = linerange(def, offset)
125-
r == nothing && continue
126-
if sf.line r
127-
mths = Base._methods_by_ftype(last(sigts), -1, typemax(UInt))
128-
if length(mths) == 1
129-
m = mths[1][3]
130-
if m methodsused
131-
push!(defs, def)
132-
push!(usrtrace, m)
133-
push!(methodsused, m)
134-
break
135-
end
136-
end
137-
end
138-
end
149+
add_by_file_line(defmap, sf)
139150
else
140151
method methodsused && continue
141152
def = Revise.get_def(method; modified_files=String[])
142153
def isa ExLike || continue
143154
push!(defs, def)
144155
push!(usrtrace, method)
145156
end
157+
else
158+
# This method was inlined and hence linfo was not available
159+
if !haskey(Revise.fileinfos, file)
160+
file = load_file(file)
161+
end
162+
haskey(Revise.fileinfos, file) || continue
163+
fi = Revise.fileinfos[file]
164+
Revise.maybe_parse_from_cache!(fi, file)
165+
for (mod, fmm) in fi.fm
166+
add_by_file_line(fmm.defmap, sf) && break
167+
end
146168
end
147169
end
148170
return usrtrace, defs
@@ -169,6 +191,7 @@ function capture_stacktrace(mod::Module, command::Expr)
169191
end
170192
errored || error("$command did not throw an error")
171193
usrtrace, defs = pregenerated_stacktrace(trace)
194+
isempty(usrtrace) && error("failed to capture any elements of the stacktrace")
172195
println(stderr, "Captured elements of stacktrace:")
173196
show(stderr, MIME("text/plain"), usrtrace)
174197
length(unique(usrtrace)) == length(usrtrace) || @error "the same method appeared twice, not supported. Try stepping into the command."

src/ui.jl

+7-1
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,13 @@ function capture_stacktrace(s)
133133
add_history(s, cmdstring)
134134
print(REPL.terminal(s), '\n')
135135
expr = Meta.parse(cmdstring)
136-
uuids = capture_stacktrace(expr)
136+
local uuids
137+
try
138+
uuids = capture_stacktrace(expr)
139+
catch err
140+
print(stderr, err)
141+
return nothing
142+
end
137143
io = IOBuffer()
138144
buf = FakePrompt(io)
139145
hp = mode(s).hist

test/runtests.jl

+44-2
Original file line numberDiff line numberDiff line change
@@ -310,7 +310,35 @@ Base.show(io::IO, ::ErrorsOnShow) = throw(ArgumentError("no show"))
310310
st = try RebuggerTesting.kwfunctop(3) catch; stacktrace(catch_backtrace()) end
311311
usrtrace, defs = Rebugger.pregenerated_stacktrace(st; topname=Symbol("macro expansion"))
312312
@test length(unique(usrtrace)) == length(usrtrace)
313-
@test usrtrace[1] == @which RebuggerTesting.kwfuncmiddle(1,1)
313+
m = @which RebuggerTesting.kwfuncmiddle(1,1)
314+
@test usrtrace[1] == m || usrtrace[2] == m
315+
316+
# A case that tests inlining and several other aspects of argument capture
317+
ex = :([1, 2, 3] .* [1, 2])
318+
# Capture the actual stack trace, trimming it to avoid anything involving the `eval` itself
319+
trace = try
320+
Core.eval(Main, ex)
321+
catch
322+
stacktrace(catch_backtrace())
323+
end
324+
i = 1
325+
while i <= length(trace)
326+
t = trace[i]
327+
if t.func == Symbol("top-level scope")
328+
deleteat!(trace, i:length(trace))
329+
end
330+
i += 1
331+
end
332+
# Get the capture from Rebugger
333+
uuids = mktemp() do path, iostacktrace
334+
redirect_stderr(iostacktrace) do
335+
Rebugger.capture_stacktrace(Main, ex)
336+
end
337+
end
338+
@test length(uuids) == length(trace)
339+
for (uuid, t) in zip(reverse(uuids), trace)
340+
@test Rebugger.stored[uuid].method.name == t.func
341+
end
314342
end
315343
end
316344

@@ -396,11 +424,25 @@ Base.show(io::IO, ::ErrorsOnShow) = throw(ArgumentError("no show"))
396424
@test countlines(io) >= 4
397425
end
398426
histdel += length(idx)
399-
@test length(idx) == 4
427+
@test length(idx) >= 5
400428
@test hist.history[idx[1]] == cmd
401429
@test occursin("error", hist.history[idx[end]])
402430
end
403431

432+
@testset "Empty stacktraces" begin
433+
cmd = "ccall(:jl_throw, Nothing, (Any,), ArgumentError(\"oops\"))"
434+
mktemp() do path, io
435+
redirect_stderr(io) do
436+
LineEdit.replace_line(mistate, cmd)
437+
@test Rebugger.capture_stacktrace(mistate) === nothing
438+
LineEdit.transition(mistate, julia_prompt)
439+
end
440+
flush(io)
441+
str = read(path, String)
442+
@test occursin("failed to capture", str)
443+
end
444+
end
445+
404446
LineEdit.edit_clear(mistate)
405447
l = length(hist.history)
406448
deleteat!(hist.history, l-histdel+1:l)

0 commit comments

Comments
 (0)