Skip to content

Commit

Permalink
Advertise lambdas as arguments to then / runRemotely (#882)
Browse files Browse the repository at this point in the history
  • Loading branch information
jglick authored Dec 18, 2024
1 parent 779b73f commit ee6dc8d
Show file tree
Hide file tree
Showing 3 changed files with 99 additions and 70 deletions.
16 changes: 13 additions & 3 deletions src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java
Original file line number Diff line number Diff line change
Expand Up @@ -745,9 +745,8 @@ public String[] getTruststoreJavaOptions() {
/**
* One step to run.
* <p>Since this thunk will be sent to a different JVM, it must be serializable.
* The test class will certainly not be serializable, so you cannot use an anonymous inner class,
* and regular lambdas also risk accidentally capturing non-serializable objects from scope.
* The friendliest idiom is a static method reference:
* The test class will certainly not be serializable, so you cannot use an anonymous inner class.
* One idiom is a static method reference:
* <pre>
* &#64;Test public void stuff() throws Throwable {
* rr.then(YourTest::_stuff);
Expand All @@ -759,6 +758,17 @@ public String[] getTruststoreJavaOptions() {
* If you need to pass and/or return values, you can still use a static method reference:
* try {@link #runRemotely(Step2)} or {@link #runRemotely(StepWithReturnAndOneArg, Serializable)} etc.
* (using {@link XStreamSerializable} as needed).
* <p>
* Alternately, you could use a lambda:
* <pre>
* &#64;Test public void stuff() throws Throwable {
* rr.then(r -> {
* // as needed
* });
* }
* </pre>
* In this case you must take care not to capture non-serializable objects from scope;
* in particular, the body must not use (named or anonymous) inner classes.
*/
@FunctionalInterface
public interface Step extends Serializable {
Expand Down
19 changes: 19 additions & 0 deletions src/main/java/org/jvnet/hudson/test/TestBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,23 @@ public boolean isApplicable(Class<? extends AbstractProject> jobType) {
}

protected Object writeReplace() { return new Object(); }

@FunctionalInterface
public interface Body {
void perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException;
}

/**
* More convenient form that can be used with a lambda.
*/
public static Builder of(Body body) {
return new TestBuilder() {
@Override
public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException {
body.perform(build, launcher, listener);
return true;
}
};
}

}
134 changes: 67 additions & 67 deletions src/test/java/org/jvnet/hudson/test/RealJenkinsRuleTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Level;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
Expand All @@ -77,6 +77,7 @@
import jenkins.model.JenkinsLocationConfiguration;
import static org.junit.Assume.assumeThat;
import org.junit.AssumptionViolatedException;
import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.jvnet.hudson.test.recipes.LocalData;
Expand Down Expand Up @@ -180,41 +181,33 @@ private static void _error(JenkinsRule r) throws Throwable {
}

@Test public void agentBuild() throws Throwable {
try (TailLog tailLog = new TailLog(rr, "p", 1).withColor(PrefixedOutputStream.Color.MAGENTA)) {
rr.then(RealJenkinsRuleTest::_agentBuild);
try (var tailLog = new TailLog(rr, "p", 1).withColor(PrefixedOutputStream.Color.MAGENTA)) {
rr.then(r -> {
var p = r.createFreeStyleProject("p");
var ran = new AtomicBoolean();
p.getBuildersList().add(TestBuilder.of((build, launcher, listener) -> ran.set(true)));
p.setAssignedNode(r.createOnlineSlave());
r.buildAndAssertSuccess(p);
assertTrue(ran.get());
});
tailLog.waitForCompletion();
}
}
private static void _agentBuild(JenkinsRule r) throws Throwable {
FreeStyleProject p = r.createFreeStyleProject("p");
AtomicReference<Boolean> ran = new AtomicReference<>(false);
p.getBuildersList().add(new TestBuilder() {
@Override public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException {
ran.set(true);
return true;
}
});
p.setAssignedNode(r.createOnlineSlave());
r.buildAndAssertSuccess(p);
assertTrue(ran.get());
}

@Test public void htmlUnit() throws Throwable {
rr.startJenkins();
rr.runRemotely(RealJenkinsRuleTest::_htmlUnit1);
rr.runRemotely(r -> {
r.jenkins.setSecurityRealm(r.createDummySecurityRealm());
r.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy().grant(Jenkins.ADMINISTER).everywhere().to("admin"));
var p = r.createFreeStyleProject("p");
p.setDescription("hello");
});
System.err.println("running against " + rr.getUrl());
rr.runRemotely(RealJenkinsRuleTest::_htmlUnit2);
}
private static void _htmlUnit1(JenkinsRule r) throws Throwable {
r.jenkins.setSecurityRealm(r.createDummySecurityRealm());
r.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy().grant(Jenkins.ADMINISTER).everywhere().to("admin"));
FreeStyleProject p = r.createFreeStyleProject("p");
p.setDescription("hello");
}
private static void _htmlUnit2(JenkinsRule r) throws Throwable {
FreeStyleProject p = r.jenkins.getItemByFullName("p", FreeStyleProject.class);
r.submit(r.createWebClient().login("admin").getPage(p, "configure").getFormByName("config"));
assertEquals("hello", p.getDescription());
rr.runRemotely(r -> {
var p = r.jenkins.getItemByFullName("p", FreeStyleProject.class);
r.submit(r.createWebClient().login("admin").getPage(p, "configure").getFormByName("config"));
assertEquals("hello", p.getDescription());
});
}

private static String _getJenkinsUrlFromRemote(JenkinsRule r) {
Expand All @@ -230,18 +223,16 @@ private static void _localData(JenkinsRule r) throws Throwable {
}

@Test public void restart() throws Throwable {
rr.then(RealJenkinsRuleTest::_restart1);
rr.then(RealJenkinsRuleTest::_restart2);
}
private static void _restart1(JenkinsRule r) throws Throwable {
assertEquals(r.jenkins.getRootUrl(), r.getURL().toString());
Files.writeString(r.jenkins.getRootDir().toPath().resolve("url.txt"), r.getURL().toString(), StandardCharsets.UTF_8);
r.jenkins.getExtensionList(ItemListener.class).add(0, new ShutdownListener());
}
private static void _restart2(JenkinsRule r) throws Throwable {
assertEquals(r.jenkins.getRootUrl(), r.getURL().toString());
assertEquals(r.jenkins.getRootUrl(), Files.readString(r.jenkins.getRootDir().toPath().resolve("url.txt"), StandardCharsets.UTF_8));
assertTrue(new File(Jenkins.get().getRootDir(), "RealJenkinsRule-ran-cleanUp").exists());
rr.then(r -> {
assertEquals(r.jenkins.getRootUrl(), r.getURL().toString());
Files.writeString(r.jenkins.getRootDir().toPath().resolve("url.txt"), r.getURL().toString(), StandardCharsets.UTF_8);
r.jenkins.getExtensionList(ItemListener.class).add(0, new ShutdownListener());
});
rr.then(r -> {
assertEquals(r.jenkins.getRootUrl(), r.getURL().toString());
assertEquals(r.jenkins.getRootUrl(), Files.readString(r.jenkins.getRootDir().toPath().resolve("url.txt"), StandardCharsets.UTF_8));
assertTrue(new File(Jenkins.get().getRootDir(), "RealJenkinsRule-ran-cleanUp").exists());
});
}
private static class ShutdownListener extends ItemListener {
private final String fileName = "RealJenkinsRule-ran-cleanUp";
Expand All @@ -263,15 +254,13 @@ private static void _stepsDoNotRunOnHttpWorkerThread(JenkinsRule r) throws Throw
}

@Test public void stepsDoNotOverwriteJenkinsLocationConfigurationIfOtherwiseSet() throws Throwable {
rr.then(RealJenkinsRuleTest::_stepsDoNotOverwriteJenkinsLocationConfigurationIfOtherwiseSet1);
rr.then(RealJenkinsRuleTest::_stepsDoNotOverwriteJenkinsLocationConfigurationIfOtherwiseSet2);
}
private static void _stepsDoNotOverwriteJenkinsLocationConfigurationIfOtherwiseSet1(JenkinsRule r) throws Throwable {
assertNotNull(JenkinsLocationConfiguration.get().getUrl());
JenkinsLocationConfiguration.get().setUrl("https://example.com/");
}
private static void _stepsDoNotOverwriteJenkinsLocationConfigurationIfOtherwiseSet2(JenkinsRule r) throws Throwable {
assertEquals("https://example.com/", JenkinsLocationConfiguration.get().getUrl());
rr.then(r -> {
assertNotNull(JenkinsLocationConfiguration.get().getUrl());
JenkinsLocationConfiguration.get().setUrl("https://example.com/");
});
rr.then(r -> {
assertEquals("https://example.com/", JenkinsLocationConfiguration.get().getUrl());
});
}

@Test
Expand Down Expand Up @@ -378,35 +367,46 @@ private static void _noDetachedPlugins(JenkinsRule r) throws Throwable {

@Test
public void safeExit() throws Throwable {
rr.then(RealJenkinsRuleTest::_safeExit);
}

private static void _safeExit(JenkinsRule r) throws Throwable {
var p = r.createFreeStyleProject();
p.getBuildersList().add(new TestBuilder() {
@Override
public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException {
Thread.sleep(Long.MAX_VALUE);
return false;
}
rr.then(r -> {
var p = r.createFreeStyleProject();
p.getBuildersList().add(TestBuilder.of((build, launcher, listener) -> Thread.sleep(Long.MAX_VALUE)));
p.scheduleBuild2(0).waitForStart();
});
p.scheduleBuild2(0).waitForStart();
}

@Test public void xStreamSerializable() throws Throwable {
rr.startJenkins();
// Neither ParametersDefinitionProperty nor ParametersAction could be passed directly.
// (In this case, ParameterDefinition and ParameterValue could have been used raw.
// But even List<ParameterValue> cannot be typed here, only e.g. ArrayList<ParameterValue>.)
var a = rr.runRemotely(RealJenkinsRuleTest::_xStreamSerializable, XStreamSerializable.of(new ParametersDefinitionProperty(new StringParameterDefinition("X", "dflt"))));
assertThat(a.object().getAllParameters(), hasSize(1));
}

private static XStreamSerializable<ParametersAction> _xStreamSerializable(JenkinsRule r, XStreamSerializable<JobProperty<? super FreeStyleProject>> prop) throws Throwable {
var prop = XStreamSerializable.of(new ParametersDefinitionProperty(new StringParameterDefinition("X", "dflt")));
// Static method handle idiom:
assertThat(rr.runRemotely(RealJenkinsRuleTest::_xStreamSerializable, prop).object().getAllParameters(), hasSize(1));
// Lambda idiom:
assertThat(rr.runRemotely(r -> {
var p = r.createFreeStyleProject();
p.addProperty(prop.object());
var b = r.buildAndAssertSuccess(p);
return XStreamSerializable.of(b.getAction(ParametersAction.class));
}).object().getAllParameters(), hasSize(1));
}

private static XStreamSerializable<ParametersAction> _xStreamSerializable(JenkinsRule r, XStreamSerializable<? extends JobProperty<? super FreeStyleProject>> prop) throws Throwable {
var p = r.createFreeStyleProject();
p.addProperty(prop.object());
var b = r.buildAndAssertSuccess(p);
return XStreamSerializable.of(b.getAction(ParametersAction.class));
}

@Ignore("inner class inside lambda breaks with an opaque NotSerializableException: RealJenkinsRuleTest; use TestBuilder.of instead")
@Test public void lambduh() throws Throwable {
rr.then(r -> {
r.createFreeStyleProject().getBuildersList().add(new TestBuilder() {
@Override public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException {
return true;
}
});
});
}

}

0 comments on commit ee6dc8d

Please sign in to comment.