diff --git a/pom.xml b/pom.xml index b7f323428..be7db1af1 100644 --- a/pom.xml +++ b/pom.xml @@ -293,7 +293,8 @@ THE SOFTWARE. maven-surefire-plugin - -Xmx256m -Djava.awt.headless=true @{jenkins.insaneHook} + + -Xmx256m -Djava.awt.headless=true @{jenkins.insaneHook} --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.io=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED ${mavenDebug} ${project.build.directory} diff --git a/src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java b/src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java index 102eedb47..65d5464de 100644 --- a/src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java +++ b/src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java @@ -158,10 +158,10 @@ *

Known limitations: *

@@ -745,7 +745,8 @@ public String[] getTruststoreJavaOptions() { /** * One step to run. *

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. + * 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: *

      * @Test public void stuff() throws Throwable {
@@ -757,6 +758,7 @@ 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). */ @FunctionalInterface public interface Step extends Serializable { diff --git a/src/main/java/org/jvnet/hudson/test/XStreamSerializable.java b/src/main/java/org/jvnet/hudson/test/XStreamSerializable.java new file mode 100644 index 000000000..d5f8c93c4 --- /dev/null +++ b/src/main/java/org/jvnet/hudson/test/XStreamSerializable.java @@ -0,0 +1,65 @@ +/* + * The MIT License + * + * Copyright 2024 CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package org.jvnet.hudson.test; + +import hudson.util.XStream2; +import java.io.Serial; +import java.io.Serializable; + +/** + * Holder for an object which is not {@link Serializable} but can be serialized safely using XStream. + * Useful for arguments and return values of {@link RealJenkinsRule#runRemotely(RealJenkinsRule.StepWithReturnAndOneArg, Serializable)} etc. + */ +public final class XStreamSerializable implements Serializable { + + @Serial + private static final long serialVersionUID = 1; + + private final String xml; + + private XStreamSerializable(String xml) { + this.xml = xml; + } + + // TODO as needed, add an optional enum for choice of Jenkins.XSTREAM2, Items.XSTREAM2, etc. + // (cannot safely use a Supplier: https://stackoverflow.com/a/27472025/12916) + private static final XStream2 XSTREAM2 = new XStream2(); + + /** + * Serializes an object to XML. + */ + public static XStreamSerializable of(T o) { + return new XStreamSerializable<>(XSTREAM2.toXML(o)); + } + + /** + * Deserializes an object from XML. + */ + @SuppressWarnings("unchecked") + public T object() { + return (T) XSTREAM2.fromXML(xml); + } + +} diff --git a/src/test/java/org/jvnet/hudson/test/RealJenkinsRuleTest.java b/src/test/java/org/jvnet/hudson/test/RealJenkinsRuleTest.java index 04eeeec23..34ae2074a 100644 --- a/src/test/java/org/jvnet/hudson/test/RealJenkinsRuleTest.java +++ b/src/test/java/org/jvnet/hudson/test/RealJenkinsRuleTest.java @@ -49,6 +49,10 @@ import hudson.model.BuildListener; import hudson.model.FreeStyleProject; import hudson.model.Item; +import hudson.model.JobProperty; +import hudson.model.ParametersAction; +import hudson.model.ParametersDefinitionProperty; +import hudson.model.StringParameterDefinition; import hudson.model.listeners.ItemListener; import hudson.util.PluginServletFilter; import java.io.ByteArrayInputStream; @@ -389,4 +393,20 @@ public boolean perform(AbstractBuild build, Launcher launcher, BuildListen 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 cannot be typed here, only e.g. ArrayList.) + var a = rr.runRemotely(RealJenkinsRuleTest::_xStreamSerializable, XStreamSerializable.of(new ParametersDefinitionProperty(new StringParameterDefinition("X", "dflt")))); + assertThat(a.object().getAllParameters(), hasSize(1)); + } + + private static XStreamSerializable _xStreamSerializable(JenkinsRule r, XStreamSerializable> prop) throws Throwable { + var p = r.createFreeStyleProject(); + p.addProperty(prop.object()); + var b = r.buildAndAssertSuccess(p); + return XStreamSerializable.of(b.getAction(ParametersAction.class)); + } + }