|
| 1 | +--- |
| 2 | +name: debug-surefire |
| 3 | +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 <host>:<port>` and drive the session from the terminal. |
| 4 | +--- |
| 5 | + |
| 6 | +# debug-surefire |
| 7 | + |
| 8 | +Run Maven Surefire tests **suspended in JDWP** so you can attach a debugger and step through the code. |
| 9 | +In headless/agent environments, **attach with `jdb`** (the JDK’s command-line debugger). |
| 10 | + |
| 11 | +## What this does |
| 12 | + |
| 13 | +- Runs `mvn test` with Surefire in debug mode via `-Dmaven.surefire.debug`. |
| 14 | +- Surefire launches the **forked test JVM** with a JDWP socket and `suspend=y`, meaning: |
| 15 | + - the forked JVM starts, |
| 16 | + - prints “Listening for transport dt_socket at address: <port>”, |
| 17 | + - then **waits** until a debugger attaches, |
| 18 | + - only then does it begin executing tests. |
| 19 | + |
| 20 | +This is important: you do **not** attach to the `mvn` process itself; you attach to the **forked JVM** running the tests. |
| 21 | + |
| 22 | +## Quick start |
| 23 | + |
| 24 | +1) Start a suspended test run |
| 25 | + |
| 26 | +- Debug a test class: |
| 27 | + - `.codex/skills/debug-surefire/scripts/debug-surefire.sh --test-class MyTest` |
| 28 | +- Debug a single test method (quote the `#`): |
| 29 | + - `.codex/skills/debug-surefire/scripts/debug-surefire.sh --test 'MyTest#shouldDoThing'` |
| 30 | +- Debug a test in a specific module: |
| 31 | + - `.codex/skills/debug-surefire/scripts/debug-surefire.sh --module core/sail/shacl --test-class ShaclSailTest` |
| 32 | + - `.codex/skills/debug-surefire/scripts/debug-surefire.sh --module rdf4j-sail-shacl --test 'ShaclSailTest#testSomething'` |
| 33 | + |
| 34 | +When the forked JVM is ready, you should see something like: |
| 35 | + |
| 36 | +- `Listening for transport dt_socket at address: 55005` |
| 37 | + |
| 38 | +2) Attach with `jdb` (preferred) |
| 39 | + |
| 40 | +In a second terminal, attach to the printed port: |
| 41 | + |
| 42 | +- Local attach (port only implies localhost): |
| 43 | + - `jdb -attach 55005` |
| 44 | +- Explicit host+port: |
| 45 | + - `jdb -attach localhost:55005` |
| 46 | + |
| 47 | +If you need source listing inside jdb, provide a sourcepath up front: |
| 48 | + |
| 49 | +- `jdb -sourcepath module/src/main/java:module/src/test/java -attach 55005` |
| 50 | + |
| 51 | +3) Set breakpoints / catches, then resume |
| 52 | + |
| 53 | +Once you’re at the `jdb` prompt, set a breakpoint (or exception catch) and continue: |
| 54 | + |
| 55 | +- `stop in com.example.MyTest.shouldDoThing` |
| 56 | +- `catch uncaught java.lang.AssertionError` |
| 57 | +- `cont` |
| 58 | + |
| 59 | +Tip: right after attaching to a just-started suspended JVM, `jdb` may show: |
| 60 | +- `No frames on the current call stack` |
| 61 | + |
| 62 | +That’s normal. The JVM hasn’t executed into any Java frames yet. Set breakpoints first, then `cont`. |
| 63 | + |
| 64 | +--- |
| 65 | + |
| 66 | +## Notes |
| 67 | + |
| 68 | +- 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. |
| 69 | +- Use `SUREFIRE_DEBUG_PORT=8000` to change the port (default: `55005`). |
| 70 | +- Use `--no-offline` / `--online` if offline (`-o`) resolution fails. |
| 71 | +- Everything after `--` is passed to Maven, e.g.: |
| 72 | + - `.codex/skills/debug-surefire/scripts/debug-surefire.sh --test-class MyTest -- -DtrimStackTrace=false -DfailIfNoTests=false -DforkCount=1 -DreuseForks=false` |
| 73 | + |
| 74 | +--- |
| 75 | + |
| 76 | +## jdb interaction guide |
| 77 | + |
| 78 | +This section is optimized for quickly getting signal from a failing unit test without drowning in framework/JDK internals. |
| 79 | + |
| 80 | +### Command cheat sheet (the ones you’ll actually use) |
| 81 | + |
| 82 | +**Start / resume** |
| 83 | +- `cont` — continue execution from current stop/breakpoint |
| 84 | +- `run` — start execution *when jdb launched the VM* (less relevant when you used `-attach`) |
| 85 | + |
| 86 | +**Breakpoints** |
| 87 | +- `stop in <class>.<method>` — break on method entry |
| 88 | +- `stop at <class>:<line>` — break at a specific line |
| 89 | +- `stop` — list all breakpoints |
| 90 | +- `clear <class>.<method>` / `clear <class>:<line>` — remove a breakpoint |
| 91 | +- `clear` — list breakpoints (same idea as `stop` listing) |
| 92 | + |
| 93 | +**Stepping** |
| 94 | +- `next` — step over calls (line-level) |
| 95 | +- `step` — step into calls (line-level) |
| 96 | +- `step up` — run until current method returns |
| 97 | +- `stepi` — step one bytecode instruction (rarely needed) |
| 98 | + |
| 99 | +**Threads / stacks** |
| 100 | +- `threads` — list threads |
| 101 | +- `thread <id>` — select default thread |
| 102 | +- `where` — stack trace for current thread |
| 103 | +- `where all` — stack traces for all threads |
| 104 | +- `up` / `down` — move the current frame up/down the stack |
| 105 | + |
| 106 | +**Inspect state** |
| 107 | +- `locals` — print locals in current frame |
| 108 | +- `print <expr>` — evaluate/print an expression (same as `eval`) |
| 109 | +- `dump <expr>` — more complete object dump |
| 110 | +- `set <lvalue> = <expr>` — mutate a variable/field (use sparingly) |
| 111 | + |
| 112 | +**Source** |
| 113 | +- `list` — show source around current line |
| 114 | +- `list <line>` or `list <method>` — show a specific region |
| 115 | +- `use <path1>:<path2>` (aka `sourcepath`) — set where jdb looks for sources |
| 116 | + |
| 117 | +**Exceptions** |
| 118 | +- `catch uncaught <exception>` — break when an uncaught exception occurs |
| 119 | +- `catch caught <exception>` — break when a caught exception occurs |
| 120 | +- `catch all <exception>` — break for both caught+uncaught |
| 121 | +- `ignore ...` — undo a catch |
| 122 | + |
| 123 | +**Reduce noise** |
| 124 | +- `exclude <pattern>,<pattern>,...` — don’t report step/method events for matching classes |
| 125 | +- `exclude none` — clear exclusions |
| 126 | + |
| 127 | +**Automation** |
| 128 | +- `monitor <command>` — run a command every time the program stops (e.g., `monitor where`) |
| 129 | +- `read <file>` — execute commands from a file |
| 130 | +- `!!` — repeat last command |
| 131 | +- `<n> <command>` — repeat command n times (e.g., `10 next`) |
| 132 | + |
| 133 | +### Efficient workflow for failing JUnit tests |
| 134 | + |
| 135 | +#### 1) Add exclusions immediately (so stepping doesn’t become a swamp) |
| 136 | + |
| 137 | +Right after connecting, set exclusions to avoid stepping into JDK/framework code: |
| 138 | + |
| 139 | +- `exclude java.*,javax.*,jdk.*,sun.*,com.sun.*,org.junit.*,org.junit.jupiter.*,org.assertj.*,org.hamcrest.*,org.mockito.*,org.apache.maven.*` |
| 140 | + |
| 141 | +You can always clear this later: |
| 142 | + |
| 143 | +- `exclude none` |
| 144 | + |
| 145 | +#### 2) Break where it matters |
| 146 | + |
| 147 | +Common breakpoint patterns: |
| 148 | + |
| 149 | +- Break at the failing test method: |
| 150 | + - `stop in com.example.MyTest.shouldDoThing` |
| 151 | + |
| 152 | +- Break inside the code under test: |
| 153 | + - `stop in com.example.service.FooService.doWork` |
| 154 | + |
| 155 | +- Break at a specific suspicious line: |
| 156 | + - `stop at com.example.service.FooService:123` |
| 157 | + |
| 158 | +If the method is overloaded, specify argument types: |
| 159 | + |
| 160 | +- `stop in com.example.FooService.doWork(int,java.lang.String)` |
| 161 | + |
| 162 | +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. |
| 163 | + |
| 164 | +#### 3) Break on the *failure*, not just your guess |
| 165 | + |
| 166 | +For unit tests, breaking on the thrown assertion/error is often faster than guessing a line. |
| 167 | + |
| 168 | +Useful catches: |
| 169 | + |
| 170 | +- Classic Java assertions / many test failures: |
| 171 | + - `catch uncaught java.lang.AssertionError` |
| 172 | + |
| 173 | +- Common in JUnit 5 assertions: |
| 174 | + - `catch uncaught org.opentest4j.AssertionFailedError` |
| 175 | + |
| 176 | +- NPE hunting: |
| 177 | + - `catch uncaught java.lang.NullPointerException` |
| 178 | + |
| 179 | +You can remove a catch with `ignore`: |
| 180 | + |
| 181 | +- `ignore uncaught java.lang.AssertionError` |
| 182 | + |
| 183 | +Tip: `catch all java.lang.Throwable` is the nuclear option. It works, but it can get loud. |
| 184 | + |
| 185 | +#### 4) Resume and drive |
| 186 | + |
| 187 | +Once breakpoints/catches are set: |
| 188 | + |
| 189 | +- `cont` |
| 190 | + |
| 191 | +When you hit a breakpoint: |
| 192 | + |
| 193 | +- `where` to see the call stack |
| 194 | +- `list` to see nearby source |
| 195 | +- `locals` to see local variables |
| 196 | +- `print someVar` or `print this.someField` to inspect |
| 197 | +- `next` to step over, `step` to step into |
| 198 | + |
| 199 | +#### 5) Find the “real” test thread quickly |
| 200 | + |
| 201 | +Surefire + JUnit can spin up multiple threads (and Maven itself has its own). When in doubt: |
| 202 | + |
| 203 | +- `threads` |
| 204 | +- `where all` |
| 205 | + |
| 206 | +Then pick the thread that’s in your test/code-under-test: |
| 207 | + |
| 208 | +- `thread <id>` |
| 209 | +- `where` |
| 210 | + |
| 211 | +#### 6) Keep the JVM count predictable |
| 212 | + |
| 213 | +Surefire forking and parallelism can make debugging confusing. If you see multiple JVMs / inconsistent behavior, pass Maven flags to keep it single and fresh: |
| 214 | + |
| 215 | +- `-DforkCount=1 -DreuseForks=false` |
| 216 | + |
| 217 | +Example: |
| 218 | + |
| 219 | +- `.codex/skills/debug-surefire/scripts/debug-surefire.sh --test 'MyTest#shouldDoThing' -- -DforkCount=1 -DreuseForks=false` |
| 220 | + |
| 221 | +#### 7) Use “thread-only” breakpoints when concurrency matters |
| 222 | + |
| 223 | +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: |
| 224 | + |
| 225 | +- `stop thread in com.example.concurrent.Worker.run` |
| 226 | + |
| 227 | +(You can also target a specific thread id with `stop thread <thread_id> in ...` if needed.) |
| 228 | + |
| 229 | +--- |
| 230 | + |
| 231 | +## jdb startup customization (optional but powerful) |
| 232 | + |
| 233 | +jdb will execute startup commands from `jdb.ini` or `.jdbrc` in either `user.home` or `user.dir`. |
| 234 | + |
| 235 | +This is handy for keeping your default exclusions/catches consistent, e.g.: |
| 236 | + |
| 237 | +- `exclude java.*,javax.*,jdk.*,sun.*,com.sun.*,org.junit.*,org.assertj.*` |
| 238 | +- `catch uncaught java.lang.AssertionError` |
| 239 | + |
| 240 | +You can also keep a project-local command file and load it on demand: |
| 241 | + |
| 242 | +- `read .codex/skills/debug-surefire/jdb.cmds` |
| 243 | + |
| 244 | +--- |
| 245 | + |
| 246 | +## Troubleshooting |
| 247 | + |
| 248 | +- **jdb can’t connect** |
| 249 | + - Confirm the port printed by the suspended JVM matches what you used in `jdb -attach`. |
| 250 | + - Confirm you’re attaching to the forked JVM port (the one that prints “Listening for transport…”), not Maven’s PID. |
| 251 | + - If running remotely (CI box / container / VM), ensure the port is reachable or forwarded. |
| 252 | + |
| 253 | +- **Breakpoints don’t hit** |
| 254 | + - Use fully qualified class names. |
| 255 | + - If overloaded, include argument types. |
| 256 | + - Use `classes` to see what’s loaded. |
| 257 | + - Try `stop at <class>:<line>` to avoid signature mismatch. |
| 258 | + |
| 259 | +- **`list` can’t find source** |
| 260 | + - Set sourcepath: |
| 261 | + - `use module/src/main/java:module/src/test/java` |
| 262 | + - Or launch jdb with `-sourcepath ...` from the start. |
0 commit comments