From 163897b238b3ce1aa109a253c7d4fe91b1426831 Mon Sep 17 00:00:00 2001 From: Jesse Glick Date: Tue, 3 Dec 2024 10:49:59 -0500 Subject: [PATCH] `XStreamSerializable` (#881) --- pom.xml | 3 +- .../jvnet/hudson/test/RealJenkinsRule.java | 8 ++- .../hudson/test/XStreamSerializable.java | 65 +++++++++++++++++++ .../hudson/test/RealJenkinsRuleTest.java | 20 ++++++ 4 files changed, 92 insertions(+), 4 deletions(-) create mode 100644 src/main/java/org/jvnet/hudson/test/XStreamSerializable.java 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: *

    *
  • Execution is a bit slower due to the overhead of launching a new JVM; and class loading overhead cannot be shared between test cases. More memory is needed. - *
  • Remote thunks must be serializable. If they need data from the test JVM, you will need to create a {@code static} nested class to package that. + *
  • Remote calls must be serializable. Use methods like {@link #runRemotely(RealJenkinsRule.StepWithReturnAndOneArg, Serializable)} and/or {@link XStreamSerializable} as needed. *
  • {@code static} state cannot be shared between the top-level test code and test bodies (though the compiler will not catch this mistake). *
  • When using a snapshot dep on Jenkins core, you must build {@code jenkins.war} to test core changes (there is no “compile-on-save” support for this). - *
  • {@link TestExtension} is not available. + *
  • {@link TestExtension} is not available (but try {@link #addSyntheticPlugin}). *
  • {@link LoggerRule} is not available, however additional loggers can be configured via {@link #withLogger(Class, Level)}}. *
  • {@link BuildWatcher} is not available, but you can use {@link TailLog} instead. *
@@ -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)); + } + }