Skip to content

Let debug_code hold back the terminated event - #122

Merged
davidanthoff merged 3 commits into
mainfrom
debug-code-termination
Aug 19, 2026
Merged

Let debug_code hold back the terminated event#122
davidanthoff merged 3 commits into
mainfrom
debug-code-termination

Conversation

@davidanthoff

Copy link
Copy Markdown
Member

Upstream half of julia-testitems/TestItemRunner.jl#107 — "Debugger does not stop at breakpoint of a testitem when using setup". TestItemControllers will use this once a release is out and the vendored copy is updated.

terminated means the debuggee has ended, and a client that hears it ends the debug session and disconnects. The session loop sends it after every :debug command, which makes a session usable exactly once.

TestItemControllers debugs a test item's @testsnippet setups and then its body as separate debug_code calls on one session. The event sent after the setup tore the session down before the body ran, so breakpoints inside the snippet worked and breakpoints in the test item itself were never hit — exactly what the issue reports.

I confirmed the mechanism before writing the fix: two debug_code calls on one session produce two terminated events.

debug_code(...; notify_termination=false) suppresses the event for a chunk that is not the last one. The default is unchanged, so every existing caller behaves exactly as before.

The test drives a real DebugSession over a loopback socket with a minimal DAP client that completes the handshake and records the events it is sent — that is what makes "once per session, not once per chunk" observable. It fails when the fix is removed.

Version bumped to 3.2.0-DEV since this adds a keyword argument.

🤖 Generated with Claude Code

`terminated` means the debuggee has ended, and a client that hears it ends the
debug session and disconnects. Sending it after every chunk of debugged code
makes a session usable exactly once.

TestItemControllers debugs a test item's `@testsnippet` setups and then its body
as separate `debug_code` calls on one session, so the event sent after the setup
tore the session down before the body ran, and breakpoints in the body were never
hit — julia-testitems/TestItemRunner.jl#107.

`debug_code(...; notify_termination=false)` suppresses the event for a chunk that
is not the last one. The default is unchanged, so a single call behaves exactly
as before.

The test drives a real session over a loopback socket with a minimal DAP client
that completes the handshake and records the events it is sent, which is what
makes "once per session, not once per chunk" observable at all.
pfitzseb
pfitzseb previously approved these changes Aug 19, 2026
Julia 1.0 has no precompile cache locking, and on Windows a `.ji` that another
process holds open cannot be replaced. This package had exactly one test item, so
one test process started and nothing raced; adding the debug session tests makes
several start together, all loading JuliaInterpreter, and whichever loses dies
with "Cannot write cache file". Nothing in the package can fix that, so CI now
runs the test items one at a time — they take seconds each, so it costs nothing.

Requires julia-testitems/testitem-workflow#8, which forwards `max-workers` to
`julia-run-testitems`; the action has always accepted it, the workflow just never
passed one through.

The README documented only the four-positional form of `debug_code`, and the
README is what people actually read, so it now covers `notify_termination` too.
@davidanthoff

Copy link
Copy Markdown
Member Author

Two follow-ups pushed.

The Windows + Julia 1.0.5 failure is not the new feature or the new test's logic — it is a precompile-cache race. The item that errored is the pre-existing test_debugengine.jl:Run some code; both new session tests passed:

Discovered 3 test items in 2 files
✗ test/test_debugengine.jl:Run some code → errored
✓ test/test_debugsession.jl:debug_code reports termination once per session, not once per chunk → passed
✓ test/test_debugsession.jl:debug_code reports termination by default → passed
Cannot write cache file "C:\Users\runneradmin\.julia\compiled\v1.0\JuliaInterpreter\PliIn.ji".

Julia 1.0 has no precompile cache locking, and on Windows a .ji another process holds open cannot be replaced. This package had exactly one test item, so one test process started and nothing raced. Going to three makes several start together, all loading JuliaInterpreter, and whichever loses the race dies. Every other platform and version passes, macOS 1.0.5 included, which is what pins it to the Windows file-locking behaviour rather than to an API gap — I also checked Sockets.listenany, Sockets.localhost, readline(io, keep=false), read(io, n) and count(==(x), v) on Julia 1.0 here and they all behave.

CI now runs the test items one at a time. That needs julia-testitems/testitem-workflow#8, which forwards max-workers to julia-run-testitems — the action has always accepted it, the shared workflow just never passed one through, unlike its four siblings threads, gc-between-testitems, memory-threshold and schedule. So this PR stays red until that merges and v2 is re-pointed.

Also added notify_termination to the README, which documented only the four-positional form.

Audit result, since you asked for one: the change is compatible with every consumer of DebugAdapter.jl.

  • All eight debug_code call sites are positional-only: VSCodeServer/src/debugger.jl:79 and :108 (@enter, @run), VSCodeServer/src/serve_notebook.jl:22 (per notebook cell), and the two in TestItemServer, in both the TIC checkout and julia-vscode's TIC submodule.
  • Every direct put! onto next_cmd omits the new field — launch_request and attach_request — so get(next_cmd, :notify_termination, true) reproduces today's behaviour exactly.
  • Base.get(::NamedTuple, ::Symbol, default) exists in Julia 1.0 (checked base/namedtuple.jl in the local 1.0.5–1.6.7 installs), so julia = "1" stays honest.
  • The :run branch is untouched; its only trigger is launch_request with noDebug.
  • Nothing consumes terminated in Julia, and there is no hand-written handler on the TypeScript side either — VS Code core ends the session on it, and the extension only observes onDidTerminateDebugSession (notebook kernel, inline debug cells). Nothing counts events per debug_code, so holding one back for a non-final chunk is invisible to all of them.

Ordering for the vendored copies: julia-vscode consumes DebugAdapter as a git submodule and picks this up on the next bump with no source change. But TestItemControllers, JuliaSessionControllers and julia-vscode's nested TIC copy vendor it as a git subtree pinned to a release tag, currently v3.1.0, and update_vendored_packages.jl only pulls tagged releases. So this needs merging and tagging 3.2.0 before julia-testitems/TestItemControllers.jl#62 can land — that PR calls the new keyword and would hit a MethodError against v3.1.0.

Worth knowing separately: julia-vscode currently ships two different DebugAdapter versions side by side — 3.1.1-DEV via the submodule for REPL/notebook/launch debugging, and 3.1.0 via the TIC subtree for test-item debugging.

davidanthoff added a commit to julia-testitems/TestItemControllers.jl that referenced this pull request Aug 19, 2026
A test item with `setup=[...]` debugs its snippets and then its own body, as
separate `debug_code` calls on one debug session. `debug_code` sends a
`terminated` event when the code it was given finishes, and a client takes that
as "the debuggee has ended" and disconnects — so the session was already gone by
the time the test item's body ran, and no breakpoint in the item was ever hit.
Breakpoints inside the snippet worked, which is what made the symptom confusing.

Snippets now pass `notify_termination=false`, so only the item's own body reports
termination.

Requires DebugAdapter 3.2, which adds the keyword argument
(julia-vscode/DebugAdapter.jl#122), to be released and vendored first.

Closes julia-testitems/TestItemRunner.jl#107
The Windows + Julia 1.0 failure was not this package's to fix. TestItemControllers
nominates one test process to activate the environment and holds the others back
until it reports, so that the test environment is precompiled exactly once — but
on Julia 1.0 to 1.8 activation built nothing, because those TestEnv variants stop
at `Pkg.activate` and only 1.9 and later finish with `Pkg._auto_precompile`. Every
process therefore reached its first `using` at the same time, with no cache file
locking in Base before 1.10 to protect them.

julia-testitems/TestItemControllers.jl#66 closes that, so capping the workers here
is unnecessary.
@davidanthoff

Copy link
Copy Markdown
Member Author

Correction: my earlier diagnosis was wrong, and the max-workers: 1 workaround is reverted.

David pointed out that TestItemControllers does serialize precompilation for old Julia, and he was right. The controller nominates one test process to activate the environment and parks the rest in ProcessWaitingForPrecompile until it reports back — precisely so the test environment is precompiled once, since Base has no cache file locking before 1.10.

What I missed is that whether that window actually builds anything depends on the vendored TestEnv shim. The julia-1.9 and later variants finish activate with Pkg._auto_precompile on the sandbox; julia-1.0 through julia-1.8 stop at Pkg.activate, and Julia 1.0's Pkg.instantiate ends at Operations.build_versions. So on 1.0–1.8 activation built nothing, the peers were released, and all three processes hit using DebugAdapter together — which is why the stack landed in run_testitem rather than in activation.

Fixed properly in julia-testitems/TestItemControllers.jl#66, which precompiles the test environment inside the window the gate already serializes. Verified on Julia 1.0 with a fixture whose only test item is default_imports=false, so nothing in the item loads the package: with the released controller neither the package nor its dependency has a cache file after a run; with the fix both do. This package's three test items then run clean from a cold cache on Julia 1.0.

So this PR needs no workflow change of its own — but it stays red until #66 ships through a TestItemControllers release and the julia-run-testitems Manifest is regenerated. The notify_termination change and the README addition are unaffected.

@davidanthoff
davidanthoff merged commit 0800003 into main Aug 19, 2026
71 of 75 checks passed
@davidanthoff
davidanthoff deleted the debug-code-termination branch August 19, 2026 21:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants