How to debug test cases with JDB and gradle?

I’m trying to learn how to use more command-line tools, mostly for my own edification, but I can’t for the life of me figure out how to debug test classes when they’ve been built with gradle. Plus, if I just use jdb with some -sourcepath and -classpath flags, I get an issue of the test class not having a main method. I assume I have to run gradle test somehow, but how can I plug jdb in and start adding breakpoints and the like? Thank you in advance for any responses.

If you do ./gradlew test --debug-jvm, then when the tests are started it waits for a debugger to attach to the process. (the details like port and so on can be configure, but by default it waits on port 5005 for the debugger to attach).

Then, while it waits, you can start jdb and tell it to attach to that remote process to debug the test execution.

I’ve read in many places now, this same exact answer. Just attach jdb to 5005. Yeah, that in itself works, but I’m still not able to figure out how to actually load the classes. While the jdb is connected to the gradle daemon, there’s really not much you can do in there. Setting breakpoints only gives you messages like, “Deferring breakpoint” and “It will be set after the class is loaded.” The question is now that it’s hooked up, how do I actually load the classes and run the test?

That has nothing to do with Gradle though.
Please read jdb documentation or ask in some jdb related community.

I actually do think this is related to gradle. From what I can tell, when trying to run gradle in debug mode using the above commands, the jdb cannot attach to a currently running daemon. Thomas Keller mentioned in this article:
https://www.thomaskeller.biz/blog/2020/11/17/debugging-with-gradle/
that running with the --no-daemon option fixes this. That at least gets us to where jdb is attached to the gradle launcher daemon. However, the actual process that I’m trying to debug is being spun off on a forked daemon with the following message:
To honour the JVM settings for this build a single-use Daemon process will be forked.
Details on that are here:

I can see both daemons:

$ ps -ef | grep Gradle
  502 89026 89024   0 10:46AM ttys001    0:03.18 java -Xmx64m -Xms64m -Dorg.gradle.appname=gradle -classpath /path/to/gradle/gradle-launcher-7.6.jar org.gradle.launcher.GradleMain project-name:test -Dorg.gradle.debug=true --no-daemon --no-build-cache --debug --tests fully.qualified.ClassName
  502 89072 89026   0 10:46AM ttys001    0:00.15 /Library/Java/JavaVirtualMachines/amazon-corretto-17.jdk/Contents/Home/bin/java --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.lang.invoke=ALL-UNNAMED --add-opens=java.prefs/java.util.prefs=ALL-UNNAMED --add-opens=java.base/java.nio.charset=ALL-UNNAMED --add-opens=java.base/java.net=ALL-UNNAMED --add-opens=java.base/java.util.concurrent.atomic=ALL-UNNAMED -XX:MaxMetaspaceSize=256m -XX:+HeapDumpOnOutOfMemoryError -Xms256m -Xmx512m -Dfile.encoding=UTF-8 -Duser.country=US -Duser.language=en -Duser.variant -agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=5005 -cp /Users/eric/.sdkman/candidates/gradle/7.6/lib/gradle-launcher-7.6.jar org.gradle.launcher.daemon.bootstrap.GradleDaemon 7.6

In this, we can see that 89026 spawned 89072. I can also verify that 89072 is listening on the intended port.

lsof -i TCP:5005    
COMMAND   PID USER   FD   TYPE             DEVICE SIZE/OFF NODE NAME
java    89072 user    5u  IPv4 0x59f2cac75b000023      0t0  TCP localhost:avt-profile-2 (LISTEN)

What is think is happening here, is the child process does not have the debug mode set. When doing “run” from jdb, the correct test will execute, but no breakpoints will be honored, because it does not appear to be in debug mode.

I guess my real question is, how can I ensure the forked daemon in this case is actually in debug mode?
or,
How can I ensure I’m actually connecting to the forked daemon, and not just the parent process?

Yes, now we are at a Gradle daemon again, but you should open a new thread and remove your messages here.
This thread is about debugging tests, not about debugging what is running in the daemon, those are greatly different topics. :slight_smile:

I think, in this case, this is very relevant. The answer above, ./gradlew test --debug-jvm by itself will not achieve the original intent: to debug a test built with gradle, using jdb. There appears to be more configuration necessary on the gradle side to achieve this.

It seems that setting the --debug-jvm option, despite being set on the gradle daemon launcher, will not be applied to the forked daemon. It would be of great help to figure out how to get around this.

Again, it is totally irrelevant.
That option is an option for the test task and affects the test worker process that is started.
It has absolutely nothing to do with the daemon and nothing more is necessary to debug a test executed through Gradle.

After digging into this deeper, it still seems to be an issue with a daemon. First, the jdb connected to the gradle daemon and all seemed fine. The daemon was listening on 5005, I could set a breakpoint, and type run, then I noticed these lines:

[org.gradle.launcher.daemon.bootstrap.DaemonOutputConsumer] daemon out: Listening for transport dt_socket at address: 5005
[org.gradle.launcher.daemon.bootstrap.DaemonOutputConsumer] daemon out: Daemon started. About to close the streams. Daemon details: 0100053334333338002463346464313638662d383137662d343163302d613162652d393030643231326138306535e0c8e1a66af54009be39aa8fb777c4310000c18a00000001000000047f00000100332f55736572732f657269632f2e677261646c652f6461656d6f6e2f372e362f6461656d6f6e2d33343333382e6f75742e6c6f67
[org.gradle.process.internal.DefaultExecHandle] Changing state to: DETACHED
[org.gradle.process.internal.DefaultExecHandle] Process 'Gradle build daemon' finished with exit value 0 (state: DETACHED)
[org.gradle.launcher.daemon.client.DefaultDaemonStarter] Gradle daemon process is now detached.

The daemon was just detaching from jdb immediately after executing that command. It would then sit and wait again:

Starting process 'Gradle Test Executor 1'....
[org.gradle.process.internal.DefaultExecHandle] Changing state to: STARTING
[org.gradle.process.internal.DefaultExecHandle] Waiting until process started: Gradle Test Executor 1.
[org.gradle.process.internal.DefaultExecHandle] Changing state to: STARTED
[org.gradle.process.internal.ExecHandleRunner] waiting until streams are handled...
[org.gradle.process.internal.DefaultExecHandle] Successfully started process 'Gradle Test Executor 1'
[system.out] Listening for transport dt_socket at address: 5005
[org.gradle.cache.internal.DefaultFileLockManager] 
[org.gradle.cache.internal.DefaultFileLockManager] Waiting to acquire shared lock on daemon addresses registry.
[org.gradle.cache.internal.DefaultFileLockManager] Lock acquired on daemon addresses registry.

I eventually figured out by trial and error that simply quitting jdb and immediately re-attaching it repaired the connection. I was then able to set breakpoints, step through code, etc.

Short take away, I’m still trying to figure out a way to just do this where --no-daemon means, no daemon but at least I’ve found a work around in the mean time.

I could easily explain it to you.
If you would just delete your messages and create your own thread.
This thread is about debugging tests executed with Gradle.
Not about debugging a Gradle task or plugin or the Gradle code itself which you seem to intend doing.
If your question would be about debugging an executed test, then the question is why you fiddle with the Gradle daemon at all which then would have nothing to do with the intended action.

If you actually try to debug a test, then you should not at all try to debug the Gradle daemon.
If you tell the test task to start the test worker with debug listening on port 5005 and additionally tell the Gradle daemon to start with debug listening on port 5005, then of course you get the behavior you described. So just stop telling the Gradle daemon to use debug mode, but only use the --debug-jvm to the test task alone and it will work.