From 8f2105b64481537acce6de3add3136ab8940ab13 Mon Sep 17 00:00:00 2001 From: Jesse Glick Date: Mon, 2 Dec 2024 13:44:43 -0500 Subject: [PATCH 1/5] `XStreamSerializable` --- pom.xml | 2 +- .../jvnet/hudson/test/RealJenkinsRule.java | 5 +- .../hudson/test/XStreamSerializable.java | 55 +++++++++++++++++++ .../hudson/test/RealJenkinsRuleTest.java | 17 ++++++ 4 files changed, 76 insertions(+), 3 deletions(-) create mode 100644 src/main/java/org/jvnet/hudson/test/XStreamSerializable.java diff --git a/pom.xml b/pom.xml index b7f323428..598d4f803 100644 --- a/pom.xml +++ b/pom.xml @@ -293,7 +293,7 @@ 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..0954745d7 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. *
@@ -757,6 +757,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..7346448cb --- /dev/null +++ b/src/main/java/org/jvnet/hudson/test/XStreamSerializable.java @@ -0,0 +1,55 @@ +/* + * 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.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 record XStreamSerializable(String xml) implements Serializable { + + // 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 XStreamSerializable(T o) { + this(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..5743049eb 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,17 @@ public boolean perform(AbstractBuild build, Launcher launcher, BuildListen p.scheduleBuild2(0).waitForStart(); } + @Test public void xStreamSerializable() throws Throwable { + rr.startJenkins(); + var a = rr.runRemotely(RealJenkinsRuleTest::_xStreamSerializable, new XStreamSerializable<>(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 new XStreamSerializable<>(b.getAction(ParametersAction.class)); + } + } From 4bcca38970f895b63f6f509a4cee81eac98b8f40 Mon Sep 17 00:00:00 2001 From: Jesse Glick Date: Mon, 2 Dec 2024 13:54:10 -0500 Subject: [PATCH 2/5] Use a `class` so that the XML representation is hidden from the API. --- .../org/jvnet/hudson/test/XStreamSerializable.java | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/jvnet/hudson/test/XStreamSerializable.java b/src/main/java/org/jvnet/hudson/test/XStreamSerializable.java index 7346448cb..fd8742174 100644 --- a/src/main/java/org/jvnet/hudson/test/XStreamSerializable.java +++ b/src/main/java/org/jvnet/hudson/test/XStreamSerializable.java @@ -25,13 +25,23 @@ 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 record XStreamSerializable(String xml) implements Serializable { +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) From 5a2993324c6733027a730bf9686b42d9fd06a4a4 Mon Sep 17 00:00:00 2001 From: Jesse Glick Date: Mon, 2 Dec 2024 13:59:54 -0500 Subject: [PATCH 3/5] Comments --- pom.xml | 1 + src/test/java/org/jvnet/hudson/test/RealJenkinsRuleTest.java | 3 +++ 2 files changed, 4 insertions(+) diff --git a/pom.xml b/pom.xml index 598d4f803..be7db1af1 100644 --- a/pom.xml +++ b/pom.xml @@ -293,6 +293,7 @@ THE SOFTWARE. maven-surefire-plugin + -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} diff --git a/src/test/java/org/jvnet/hudson/test/RealJenkinsRuleTest.java b/src/test/java/org/jvnet/hudson/test/RealJenkinsRuleTest.java index 5743049eb..96cd6a4c2 100644 --- a/src/test/java/org/jvnet/hudson/test/RealJenkinsRuleTest.java +++ b/src/test/java/org/jvnet/hudson/test/RealJenkinsRuleTest.java @@ -395,6 +395,9 @@ public boolean perform(AbstractBuild build, Launcher launcher, BuildListen @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, new XStreamSerializable<>(new ParametersDefinitionProperty(new StringParameterDefinition("X", "dflt")))); assertThat(a.object().getAllParameters(), hasSize(1)); } From 2940c2fc6d93d2e62481d984d36632618939c67f Mon Sep 17 00:00:00 2001 From: Jesse Glick Date: Mon, 2 Dec 2024 14:06:55 -0500 Subject: [PATCH 4/5] Another Javadoc update --- src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java b/src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java index 0954745d7..65d5464de 100644 --- a/src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java +++ b/src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java @@ -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 {

From c6b9b90954e34828c36c50528c821b59210784ae Mon Sep 17 00:00:00 2001
From: Jesse Glick 
Date: Tue, 3 Dec 2024 08:23:29 -0500
Subject: [PATCH 5/5] Switch to a factory method
 https://github.com/jenkinsci/jenkins-test-harness/pull/881#discussion_r1867249740

---
 src/main/java/org/jvnet/hudson/test/XStreamSerializable.java | 4 ++--
 src/test/java/org/jvnet/hudson/test/RealJenkinsRuleTest.java | 4 ++--
 2 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/src/main/java/org/jvnet/hudson/test/XStreamSerializable.java b/src/main/java/org/jvnet/hudson/test/XStreamSerializable.java
index fd8742174..d5f8c93c4 100644
--- a/src/main/java/org/jvnet/hudson/test/XStreamSerializable.java
+++ b/src/main/java/org/jvnet/hudson/test/XStreamSerializable.java
@@ -50,8 +50,8 @@ private XStreamSerializable(String xml) {
     /**
      * Serializes an object to XML.
      */
-    public XStreamSerializable(T o) {
-        this(XSTREAM2.toXML(o));
+    public static  XStreamSerializable of(T o) {
+        return new XStreamSerializable<>(XSTREAM2.toXML(o));
     }
 
     /**
diff --git a/src/test/java/org/jvnet/hudson/test/RealJenkinsRuleTest.java b/src/test/java/org/jvnet/hudson/test/RealJenkinsRuleTest.java
index 96cd6a4c2..34ae2074a 100644
--- a/src/test/java/org/jvnet/hudson/test/RealJenkinsRuleTest.java
+++ b/src/test/java/org/jvnet/hudson/test/RealJenkinsRuleTest.java
@@ -398,7 +398,7 @@ public boolean perform(AbstractBuild build, Launcher launcher, BuildListen
         // 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, new XStreamSerializable<>(new ParametersDefinitionProperty(new StringParameterDefinition("X", "dflt"))));
+        var a = rr.runRemotely(RealJenkinsRuleTest::_xStreamSerializable, XStreamSerializable.of(new ParametersDefinitionProperty(new StringParameterDefinition("X", "dflt"))));
         assertThat(a.object().getAllParameters(), hasSize(1));
     }
 
@@ -406,7 +406,7 @@ private static XStreamSerializable _xStreamSerializable(Jenkin
         var p = r.createFreeStyleProject();
         p.addProperty(prop.object());
         var b = r.buildAndAssertSuccess(p);
-        return new XStreamSerializable<>(b.getAction(ParametersAction.class));
+        return XStreamSerializable.of(b.getAction(ParametersAction.class));
     }
 
 }