Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name = "DebugAdapter"
uuid = "17994d07-08fe-42cc-bc1b-7af499b1ea47"
version = "3.1.1-DEV"
version = "3.2.0-DEV"

[deps]
Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f"
Expand Down
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,12 @@ filepath = joinpath(homedir(), "something.jl") # This is the filepath that shoul

DebugAdapter.debug_code(session, mod, code, filepath)
```

`debug_code` sends a `terminated` event once the code has finished, and a client takes
that as "the debuggee has ended" and disconnects. To debug more than one piece of code in
one session, pass `notify_termination=false` on every call but the last:

```julia
DebugAdapter.debug_code(session, mod, setup_code, setup_path; notify_termination=false)
DebugAdapter.debug_code(session, mod, code, filepath)
```
23 changes: 20 additions & 3 deletions src/packagedef.jl
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@
break
elseif next_cmd.cmd == :run
try
Main.include(next_cmd.program)

Check warning on line 123 in src/packagedef.jl

View workflow job for this annotation

GitHub Actions / julia-ci / lint

missing_reference

Missing reference: include
catch err
Base.display_error(stderr, err, catch_backtrace())
end
Expand Down Expand Up @@ -163,7 +163,14 @@

debug_session.debug_engine = nothing

DAPRPC.send(endpoint, terminated_notification_type, TerminatedEventArguments(false))
# `terminated` means the debuggee has ended, and a client that hears it
# ends the debug session and disconnects. A caller that is going to debug
# more code in this session — TestItemControllers runs a test item's
# `@testsnippet` setups and then its body, as separate chunks — therefore
# has to keep it back until the last one.
if get(next_cmd, :notify_termination, true)
DAPRPC.send(endpoint, terminated_notification_type, TerminatedEventArguments(false))
end

put!(debug_session.finished_execution, true)
else
Expand All @@ -181,10 +188,20 @@
end
end

function debug_code(debug_session::DebugSession, mod::Module, code::String, filename::String)
"""
debug_code(debug_session, mod, code, filename; notify_termination=true)

Debug `code` in `mod` and return once it has finished.

`notify_termination=false` suppresses the `terminated` event that would otherwise be sent
when the code finishes. Pass it when more code is going to be debugged in the same session:
a client takes `terminated` as "the debuggee has ended" and disconnects, so a chunk that is
not the last one must not send it.
"""
function debug_code(debug_session::DebugSession, mod::Module, code::String, filename::String; notify_termination::Bool=true)
fetch(debug_session.attached)

put!(debug_session.next_cmd, (cmd=:debug, mod=mod, code=code, filename=filename))
put!(debug_session.next_cmd, (cmd=:debug, mod=mod, code=code, filename=filename, notify_termination=notify_termination))

take!(debug_session.finished_execution)
end
Expand Down
113 changes: 113 additions & 0 deletions test/test_debugsession.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
@testsnippet DapClient begin
import Sockets, JSON

"""
with_debug_session(f) -> (events, result)

Run a `DebugSession` over a pipe with a minimal DAP client attached, hand `f` the
session so it can debug code on it, and return the events the client saw along with
whatever `f` returned.

The client is only as complete as these tests need: it completes the handshake and
records every event it is sent.
"""
function with_debug_session(f)
# A loopback socket rather than the named pipe the real adapter is given, because
# the session only ever sees an `IO` and a port needs no cleanup or platform case.
port, server = Sockets.listenany(Sockets.localhost, 0)

result = Ref{Any}(nothing)

server_task = @async begin
conn = Sockets.accept(server)
session = DebugAdapter.DebugSession(conn)
session_task = @async DebugAdapter.run(session)
try
result[] = f(session)
finally
close(session)
wait(session_task)
end
end

client = Sockets.connect(Sockets.localhost, port)
events = String[]
seq = Ref(0)

function request(command, arguments=Dict{String,Any}())
seq[] += 1
payload = JSON.json(Dict{String,Any}(
"seq" => seq[],
"type" => "request",
"command" => command,
"arguments" => arguments,
))
write(client, "Content-Length: $(sizeof(payload))\r\n\r\n", payload)
flush(client)
end

reader = @async while isopen(client)
line = readline(client, keep=false)
startswith(line, "Content-Length:") || continue
n = parse(Int, strip(split(line, ':')[2]))
readline(client) # the blank line between header and body
msg = JSON.parse(String(read(client, n)))
msg["type"] == "event" && push!(events, msg["event"])
end

try
request("initialize", Dict{String,Any}("adapterID" => "julia"))
# The handshake is only ordered by what the adapter waits on, and `attach`
# fulfills what `debug_code` blocks on, so a short wait between the two is
# enough to keep them in order.
sleep(0.5)
request("attach", Dict{String,Any}("stopOnEntry" => false))
sleep(0.5)
request("configurationDone", Dict{String,Any}())

wait(server_task)
sleep(0.5)
finally
close(client)
close(server)
end

return (events, result[])
end
end

@testitem "debug_code reports termination once per session, not once per chunk" setup=[DapClient] begin
# `terminated` means the debuggee has ended, and a client that hears it ends the debug
# session and disconnects. TestItemControllers debugs a test item's `@testsnippet`
# setups and then its body as separate `debug_code` calls, so sending the event after
# the setup tore the session down before the body ever ran and breakpoints in the body
# were never hit (julia-testitems/TestItemRunner.jl#107).
module TerminationTarget
first_ran = false
second_ran = false
end

events, _ = with_debug_session() do session
DebugAdapter.debug_code(session, TerminationTarget, "first_ran = true\n", "setup.jl"; notify_termination=false)
DebugAdapter.debug_code(session, TerminationTarget, "second_ran = true\n", "body.jl")
end

# Both chunks ran, in the same session
@test TerminationTarget.first_ran == true
@test TerminationTarget.second_ran == true

@test count(==("terminated"), events) == 1
end

@testitem "debug_code reports termination by default" setup=[DapClient] begin
module DefaultTerminationTarget
ran = false
end

events, _ = with_debug_session() do session
DebugAdapter.debug_code(session, DefaultTerminationTarget, "ran = true\n", "body.jl")
end

@test DefaultTerminationTarget.ran == true
@test count(==("terminated"), events) == 1
end
Loading