Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Advertise lambdas as arguments to then / runRemotely #882

Merged
merged 13 commits into from
Dec 18, 2024
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;
}
});
});
}

}
Loading