diff --git a/src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java b/src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java index 87c520229..bd4428d17 100644 --- a/src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java +++ b/src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java @@ -186,6 +186,10 @@ public final class RealJenkinsRule implements TestRule { private Map loggers = new HashMap<>(); + private int debugPort = 0; + private boolean debugServer = true; + private boolean debugSuspend; + // TODO may need to be relaxed for Gradle-based plugins private static final Pattern SNAPSHOT_INDEX_JELLY = Pattern.compile("(file:/.+/target)/classes/index.jelly"); @@ -330,6 +334,57 @@ public RealJenkinsRule withHttpListenAddress(String httpListenAddress) { return this; } + /** + * Allows usage of a static debug port instead of a random one. + *

+ * This allows to use predefined debug configurations in the IDE. + *

+ * Typical usage is in a base test class where multiple named controller instances are defined with fixed ports + * + *

+     * public RealJenkinsRule cc1 = new RealJenkinsRule().withName("cc1").withDebugPort(4001).withDebugServer(false);
+     *
+     * public RealJenkinsRule cc2 = new RealJenkinsRule().withName("cc2").withDebugPort(4002).withDebugServer(false);
+     * 
+ * + * Then have debug configurations in the IDE set for ports + * + *

+ * This allows for debugger to reconnect in scenarios where restarts of controllers are involved. + * + * @param debugPort the TCP port to use for debugging this Jenkins instance. Between 0 (random) and 65536 (excluded). + */ + public RealJenkinsRule withDebugPort(int debugPort) { + if (debugPort < 0) throw new IllegalArgumentException("debugPort must be positive"); + if (!(debugPort < 65536)) throw new IllegalArgumentException("debugPort must be a valid TCP port (< 65536)"); + this.debugPort = debugPort; + return this; + } + /** + * Allows to use debug in server mode or client mode. Client mode is friendlier to controller restarts. + * + * @see #withDebugPort(int) + * + * @param debugServer true to use server=y, false to use server=n + */ + public RealJenkinsRule withDebugServer(boolean debugServer) { + this.debugServer = debugServer; + return this; + } + + /** + * Whether to suspend the controller VM on startup until debugger is connected. Defaults to false. + * @param debugSuspend true to suspend the controller VM on startup until debugger is connected. + */ + public RealJenkinsRule withDebugSuspend(boolean debugSuspend) { + this.debugSuspend = debugSuspend; + return this; + } + /** * The intended use case for this is to use the plugins bundled into the war {@link RealJenkinsRule#withWar(File)} * instead of the plugins in the pom. A typical scenario for this feature is a test which does not live inside a @@ -562,7 +617,10 @@ public void startJenkins() throws Throwable { portFile = new File(home, "jenkins-port.txt"); argv.add("-Dwinstone.portFileName=" + portFile); if (new DisableOnDebug(null).isDebugging()) { - argv.add("-agentlib:jdwp=transport=dt_socket,server=y"); + argv.add("-agentlib:jdwp=transport=dt_socket" + + ",server=" + (debugServer ? "y" : "n") + + ",suspend=" + (debugSuspend ? "y" : "n") + + (debugPort > 0 ? ",address=" + httpListenAddress + ":" + debugPort : "")); } argv.addAll(javaOptions); diff --git a/src/test/java/org/jvnet/hudson/test/RealJenkinsRuleTest.java b/src/test/java/org/jvnet/hudson/test/RealJenkinsRuleTest.java index 4fb38f153..945bf4c7c 100644 --- a/src/test/java/org/jvnet/hudson/test/RealJenkinsRuleTest.java +++ b/src/test/java/org/jvnet/hudson/test/RealJenkinsRuleTest.java @@ -72,7 +72,7 @@ public class RealJenkinsRuleTest { // TODO addPlugins does not currently take effect when used inside test method - @Rule public RealJenkinsRule rr = new RealJenkinsRule().addPlugins("plugins/structs.hpi"); + @Rule public RealJenkinsRule rr = new RealJenkinsRule().addPlugins("plugins/structs.hpi").withDebugPort(4001).withDebugServer(false); @Rule public RealJenkinsRule rrWithFailure = new RealJenkinsRule().addPlugins("plugins/failure.hpi"); @Test public void smokes() throws Throwable {