--- name: debug-surefire description: Debug Maven Surefire unit tests by running them in JDWP "wait for debugger" mode (`-Dmaven.surefire.debug`) and attaching to the forked test JVM using **jdb** (preferred for CLI/agent debugging), IntelliJ, or VS Code. Use when asked to debug/step through a failing JUnit test, attach a debugger to a Maven test run, or run `mvn test -Dtest=Class[#method]` suspended on a port (including multi-module `-pl` runs). The JVM will block at startup until a debugger attaches; the agent should attach with `jdb -attach :` and drive the session from the terminal. --- # debug-surefire Run Maven Surefire tests **suspended in JDWP** so you can attach a debugger and step through the code. In headless/agent environments, **attach with `jdb`** (the JDK’s command-line debugger). ## What this does - Runs `mvn test` with Surefire in debug mode via `-Dmaven.surefire.debug`. - Surefire launches the **forked test JVM** with a JDWP socket and `suspend=y`, meaning: - the forked JVM starts, - prints “Listening for transport dt_socket at address: ”, - then **waits** until a debugger attaches, - only then does it begin executing tests. This is important: you do **not** attach to the `mvn` process itself; you attach to the **forked JVM** running the tests. ## Quick start 1) Start a suspended test run - Debug a test class: - `.codex/skills/debug-surefire/scripts/debug-surefire.sh --test-class MyTest` - Debug a single test method (quote the `#`): - `.codex/skills/debug-surefire/scripts/debug-surefire.sh --test 'MyTest#shouldDoThing'` - Debug a test in a specific module: - `.codex/skills/debug-surefire/scripts/debug-surefire.sh --module core/sail/shacl --test-class ShaclSailTest` - `.codex/skills/debug-surefire/scripts/debug-surefire.sh --module rdf4j-sail-shacl --test 'ShaclSailTest#testSomething'` When the forked JVM is ready, you should see something like: - `Listening for transport dt_socket at address: 55005` 2) Attach with `jdb` (preferred) In a second terminal, attach to the printed port: - Local attach (port only implies localhost): - `jdb -attach 55005` - Explicit host+port: - `jdb -attach localhost:55005` If you need source listing inside jdb, provide a sourcepath up front: - `jdb -sourcepath module/src/main/java:module/src/test/java -attach 55005` 3) Set breakpoints / catches, then resume Once you’re at the `jdb` prompt, set a breakpoint (or exception catch) and continue: - `stop in com.example.MyTest.shouldDoThing` - `catch uncaught java.lang.AssertionError` - `cont` Tip: right after attaching to a just-started suspended JVM, `jdb` may show: - `No frames on the current call stack` That’s normal. The JVM hasn’t executed into any Java frames yet. Set breakpoints first, then `cont`. --- ## Notes - The script runs a fast pre-test install (`-Pquick` into the repo-local `.m2_repo`) and then runs `mvn test` with Surefire in debug mode. - Use `SUREFIRE_DEBUG_PORT=8000` to change the port (default: `55005`). - Use `--no-offline` / `--online` if offline (`-o`) resolution fails. - Everything after `--` is passed to Maven, e.g.: - `.codex/skills/debug-surefire/scripts/debug-surefire.sh --test-class MyTest -- -DtrimStackTrace=false -DfailIfNoTests=false -DforkCount=1 -DreuseForks=false` --- ## jdb interaction guide This section is optimized for quickly getting signal from a failing unit test without drowning in framework/JDK internals. ### Command cheat sheet (the ones you’ll actually use) **Start / resume** - `cont` — continue execution from current stop/breakpoint - `run` — start execution *when jdb launched the VM* (less relevant when you used `-attach`) **Breakpoints** - `stop in .` — break on method entry - `stop at :` — break at a specific line - `stop` — list all breakpoints - `clear .` / `clear :` — remove a breakpoint - `clear` — list breakpoints (same idea as `stop` listing) **Stepping** - `next` — step over calls (line-level) - `step` — step into calls (line-level) - `step up` — run until current method returns - `stepi` — step one bytecode instruction (rarely needed) **Threads / stacks** - `threads` — list threads - `thread ` — select default thread - `where` — stack trace for current thread - `where all` — stack traces for all threads - `up` / `down` — move the current frame up/down the stack **Inspect state** - `locals` — print locals in current frame - `print ` — evaluate/print an expression (same as `eval`) - `dump ` — more complete object dump - `set = ` — mutate a variable/field (use sparingly) **Source** - `list` — show source around current line - `list ` or `list ` — show a specific region - `use :` (aka `sourcepath`) — set where jdb looks for sources **Exceptions** - `catch uncaught ` — break when an uncaught exception occurs - `catch caught ` — break when a caught exception occurs - `catch all ` — break for both caught+uncaught - `ignore ...` — undo a catch **Reduce noise** - `exclude ,,...` — don’t report step/method events for matching classes - `exclude none` — clear exclusions **Automation** - `monitor ` — run a command every time the program stops (e.g., `monitor where`) - `read ` — execute commands from a file - `!!` — repeat last command - ` ` — repeat command n times (e.g., `10 next`) ### Efficient workflow for failing JUnit tests #### 1) Add exclusions immediately (so stepping doesn’t become a swamp) Right after connecting, set exclusions to avoid stepping into JDK/framework code: - `exclude java.*,javax.*,jdk.*,sun.*,com.sun.*,org.junit.*,org.junit.jupiter.*,org.assertj.*,org.hamcrest.*,org.mockito.*,org.apache.maven.*` You can always clear this later: - `exclude none` #### 2) Break where it matters Common breakpoint patterns: - Break at the failing test method: - `stop in com.example.MyTest.shouldDoThing` - Break inside the code under test: - `stop in com.example.service.FooService.doWork` - Break at a specific suspicious line: - `stop at com.example.service.FooService:123` If the method is overloaded, specify argument types: - `stop in com.example.FooService.doWork(int,java.lang.String)` Note: `jdb` supports **deferred breakpoints**. If the class isn’t loaded yet, it will still accept the breakpoint and activate it when the class loads. #### 3) Break on the *failure*, not just your guess For unit tests, breaking on the thrown assertion/error is often faster than guessing a line. Useful catches: - Classic Java assertions / many test failures: - `catch uncaught java.lang.AssertionError` - Common in JUnit 5 assertions: - `catch uncaught org.opentest4j.AssertionFailedError` - NPE hunting: - `catch uncaught java.lang.NullPointerException` You can remove a catch with `ignore`: - `ignore uncaught java.lang.AssertionError` Tip: `catch all java.lang.Throwable` is the nuclear option. It works, but it can get loud. #### 4) Resume and drive Once breakpoints/catches are set: - `cont` When you hit a breakpoint: - `where` to see the call stack - `list` to see nearby source - `locals` to see local variables - `print someVar` or `print this.someField` to inspect - `next` to step over, `step` to step into #### 5) Find the “real” test thread quickly Surefire + JUnit can spin up multiple threads (and Maven itself has its own). When in doubt: - `threads` - `where all` Then pick the thread that’s in your test/code-under-test: - `thread ` - `where` #### 6) Keep the JVM count predictable Surefire forking and parallelism can make debugging confusing. If you see multiple JVMs / inconsistent behavior, pass Maven flags to keep it single and fresh: - `-DforkCount=1 -DreuseForks=false` Example: - `.codex/skills/debug-surefire/scripts/debug-surefire.sh --test 'MyTest#shouldDoThing' -- -DforkCount=1 -DreuseForks=false` #### 7) Use “thread-only” breakpoints when concurrency matters By default, `jdb` breakpoints suspend all threads, which can create deadlocks in concurrent tests. You can tell jdb to suspend only the thread that hits the breakpoint: - `stop thread in com.example.concurrent.Worker.run` (You can also target a specific thread id with `stop thread in ...` if needed.) --- ## jdb startup customization (optional but powerful) jdb will execute startup commands from `jdb.ini` or `.jdbrc` in either `user.home` or `user.dir`. This is handy for keeping your default exclusions/catches consistent, e.g.: - `exclude java.*,javax.*,jdk.*,sun.*,com.sun.*,org.junit.*,org.assertj.*` - `catch uncaught java.lang.AssertionError` You can also keep a project-local command file and load it on demand: - `read .codex/skills/debug-surefire/jdb.cmds` --- ## Troubleshooting - **jdb can’t connect** - Confirm the port printed by the suspended JVM matches what you used in `jdb -attach`. - Confirm you’re attaching to the forked JVM port (the one that prints “Listening for transport…”), not Maven’s PID. - If running remotely (CI box / container / VM), ensure the port is reachable or forwarded. - **Breakpoints don’t hit** - Use fully qualified class names. - If overloaded, include argument types. - Use `classes` to see what’s loaded. - Try `stop at :` to avoid signature mismatch. - **`list` can’t find source** - Set sourcepath: - `use module/src/main/java:module/src/test/java` - Or launch jdb with `-sourcepath ...` from the start.