Skip to content
This repository has been archived by the owner on Dec 15, 2021. It is now read-only.

Commit

Permalink
Merge pull request #58 from jglick/DescribableHelper-default-JENKINS-…
Browse files Browse the repository at this point in the history
…25779

[JENKINS-25779] DescribableHelper.uninstantiate and default values
  • Loading branch information
jglick committed Feb 24, 2015
2 parents 40b9acf + 6e04069 commit 1714a18
Show file tree
Hide file tree
Showing 6 changed files with 196 additions and 17 deletions.
1 change: 1 addition & 0 deletions CHANGES.md
Expand Up @@ -5,6 +5,7 @@ Only noting significant user-visible or major API changes, not internal code cle
## 1.3 (upcoming)

### User changes
* JENKINS-25779: snippet generator now omits default values of complex steps.
* Ability to configure project display name.
* Fixing `java.io.NotSerializableException: org.jenkinsci.plugins.workflow.support.steps.StageStepExecution$CanceledCause` thrown from certain scripts using `stage`.
* JENKINS-27052: `stage` step did not prevent a third build from entering a stage after a second was unblocked by a first leaving it.
Expand Down
Expand Up @@ -64,7 +64,6 @@ static String object2Groovy(Object o) throws UnsupportedOperationException {
StringBuilder b = new StringBuilder(d.getFunctionName());
Step step = (Step) o;
Map<String,Object> args = new TreeMap<String,Object>(d.defineArguments(step));
args.values().removeAll(Collections.singleton(null)); // do not write null values
boolean first = true;
boolean singleMap = args.size() == 1 && args.values().iterator().next() instanceof Map;
for (Map.Entry<String,Object> entry : args.entrySet()) {
Expand Down
Expand Up @@ -46,10 +46,13 @@
import org.jenkinsci.plugins.workflow.support.steps.StageStep;
import org.jenkinsci.plugins.workflow.support.steps.WorkspaceStep;
import org.jenkinsci.plugins.workflow.support.steps.build.BuildTriggerStep;
import org.jenkinsci.plugins.workflow.support.steps.input.InputStep;
import org.junit.Test;
import static org.junit.Assert.*;
import org.junit.ClassRule;
import org.junit.Ignore;
import org.jvnet.hudson.test.Email;
import org.jvnet.hudson.test.Issue;
import org.jvnet.hudson.test.JenkinsRule;

public class SnippetizerTest {
Expand All @@ -72,7 +75,12 @@ public class SnippetizerTest {
@Test public void coreStep() throws Exception {
ArtifactArchiver aa = new ArtifactArchiver("x.jar");
aa.setAllowEmptyArchive(true);
assertRoundTrip(new CoreStep(aa), "step([$class: 'ArtifactArchiver', allowEmptyArchive: true, artifacts: 'x.jar', defaultExcludes: true, excludes: '', fingerprint: false, onlyIfSuccessful: false])");
assertRoundTrip(new CoreStep(aa), "step([$class: 'ArtifactArchiver', allowEmptyArchive: true, artifacts: 'x.jar'])");
}

@Ignore("TODO until 1.601+ expected:<step[([$class: 'ArtifactArchiver', artifacts: 'x.jar'])]> but was:<step[ <object of type hudson.tasks.ArtifactArchiver>]>")
@Test public void coreStep2() throws Exception {
assertRoundTrip(new CoreStep(new ArtifactArchiver("x.jar")), "step([$class: 'ArtifactArchiver', artifacts: 'x.jar'])");
}

@Test public void blockSteps() throws Exception {
Expand Down Expand Up @@ -100,6 +108,11 @@ public class SnippetizerTest {
*/
}

@Issue("JENKINS-25779")
@Test public void defaultValues() throws Exception {
assertRoundTrip(new InputStep("Ready?"), "input 'Ready?'");
}

private static void assertRoundTrip(Step step, String expected) throws Exception {
assertEquals(expected, Snippetizer.object2Groovy(step));
GroovyShell shell = new GroovyShell(r.jenkins.getPluginManager().uberClassLoader);
Expand Down
Expand Up @@ -38,18 +38,23 @@
import java.lang.reflect.Type;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.atomic.AtomicReference;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.Nonnull;
import jenkins.model.Jenkins;
import net.java.sezpoz.Index;
import net.java.sezpoz.IndexItem;
import org.apache.commons.lang.ObjectUtils;
import org.codehaus.groovy.reflection.ReflectionCache;
import org.kohsuke.stapler.ClassDescriptor;
import org.kohsuke.stapler.DataBoundConstructor;
Expand All @@ -62,6 +67,8 @@
*/
public class DescribableHelper {

private static final Logger LOG = Logger.getLogger(DescribableHelper.class.getName());

/**
* Creates an instance of a class via {@link DataBoundConstructor} and {@link DataBoundSetter}.
* <p>The arguments may be primitives (as wrappers) or {@link String}s if that is their declared type.
Expand Down Expand Up @@ -103,22 +110,65 @@ public static Map<String,Object> uninstantiate(Object o) throws UnsupportedOpera
for (String name : names) {
inspect(r, o, clazz, name);
}
r.values().removeAll(Collections.singleton(null));
Map<String,Object> constructorOnlyDataBoundProps = new TreeMap<String,Object>(r);
List<String> dataBoundSetters = new ArrayList<String>();
for (Class<?> c = clazz; c != null; c = c.getSuperclass()) {
for (Field f : c.getDeclaredFields()) {
if (f.isAnnotationPresent(DataBoundSetter.class)) {
inspect(r, o, clazz, f.getName());
String field = f.getName();
dataBoundSetters.add(field);
inspect(r, o, clazz, field);
}
}
for (Method m : c.getDeclaredMethods()) {
if (m.isAnnotationPresent(DataBoundSetter.class) && m.getName().startsWith("set")) {
inspect(r, o, clazz, Introspector.decapitalize(m.getName().substring(3)));
String field = Introspector.decapitalize(m.getName().substring(3));
dataBoundSetters.add(field);
inspect(r, o, clazz, field);
}
}
}
r.values().removeAll(Collections.singleton(null));
clearDefaultSetters(clazz, r, constructorOnlyDataBoundProps, dataBoundSetters);
return r;
}

/**
* Removes configuration of any properties based on {@link DataBoundSetter} which appear unmodified from the default.
* @param clazz the class of {@code o}
* @param allDataBoundProps all its properties, including those from its {@link DataBoundConstructor} as well as any {@link DataBoundSetter}s; some of the latter might be deleted
* @param constructorOnlyDataBoundProps properties from {@link DataBoundConstructor} only
* @param dataBoundSetters a list of property names marked with {@link DataBoundSetter}
*/
private static void clearDefaultSetters(Class<?> clazz, Map<String,Object> allDataBoundProps, Map<String,Object> constructorOnlyDataBoundProps, Collection<String> dataBoundSetters) {
if (dataBoundSetters.isEmpty()) {
return;
}
Object control;
try {
control = instantiate(clazz, constructorOnlyDataBoundProps);
} catch (Exception x) {
LOG.log(Level.WARNING, "Cannot create control version of " + clazz + " using " + constructorOnlyDataBoundProps, x);
return;
}
Map<String,Object> fromControl = new HashMap<String,Object>(constructorOnlyDataBoundProps);
Iterator<String> fields = dataBoundSetters.iterator();
while (fields.hasNext()) {
String field = fields.next();
try {
inspect(fromControl, control, clazz, field);
} catch (RuntimeException x) {
LOG.log(Level.WARNING, "Failed to check property " + field + " of " + clazz + " on " + control, x);
fields.remove();
}
}
for (String field : dataBoundSetters) {
if (ObjectUtils.equals(allDataBoundProps.get(field), fromControl.get(field))) {
allDataBoundProps.remove(field);
}
}
}

private static final String CLAZZ = "$class";

private static Object[] buildArguments(Class<?> clazz, Map<String,?> arguments, Type[] types, String[] names, boolean callEvenIfNoArgs) throws Exception {
Expand Down Expand Up @@ -296,6 +346,9 @@ private static Object uncoerce(Object o, Type type) {
return nested;
} catch (UnsupportedOperationException x) {
// then leave it raw
if (!(x.getCause() instanceof NoStaplerConstructorException)) {
LOG.log(Level.WARNING, "failed to uncoerce " + o, x);
}
}
}
return o;
Expand Down
Expand Up @@ -40,9 +40,11 @@
import static org.junit.Assert.*;
import org.junit.BeforeClass;
import org.junit.Test;
import org.jvnet.hudson.test.Issue;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.DataBoundSetter;

@SuppressWarnings("unchecked") // generic array construction
public class DescribableHelperTest {

@BeforeClass public static void isUnitTest() {
Expand All @@ -58,7 +60,7 @@ public class DescribableHelperTest {
}

@Test public void uninstantiate() throws Exception {
assertEquals("{flag=true, shorty=0, text=stuff}", DescribableHelper.uninstantiate(new C("stuff", true)).toString());
assertEquals("{flag=true, text=stuff}", DescribableHelper.uninstantiate(new C("stuff", true)).toString());
I i = new I("stuff");
i.setFlag(true);
i.text = "more";
Expand Down Expand Up @@ -120,7 +122,6 @@ public boolean isFlag() {
}
}

@SuppressWarnings("unchecked")
@Test public void findSubtypes() throws Exception {
assertEquals(new HashSet<Class<?>>(Arrays.asList(Impl1.class, Impl2.class)), DescribableHelper.findSubtypes(Base.class));
assertEquals(Collections.singleton(Impl1.class), DescribableHelper.findSubtypes(Marker.class));
Expand All @@ -139,7 +140,7 @@ public boolean isFlag() {
@Test public void nestedStructs() throws Exception {
roundTrip(UsesBase.class, map("base", map("$class", "Impl1", "text", "hello")));
roundTrip(UsesBase.class, map("base", map("$class", "Impl2", "flag", true)));
roundTrip(UsesImpl2.class, map("impl2", map("flag", false)));
roundTrip(UsesImpl2.class, map("impl2", map()));
}

public static class UsesBase {
Expand Down Expand Up @@ -267,9 +268,8 @@ public List<String> getStrings() {
}
}

@SuppressWarnings("unchecked")
@Test public void structArrayHomo() throws Exception {
roundTrip(UsesStructArrayHomo.class, map("impls", Arrays.asList(map("flag", false), map("flag", true))), "UsesStructArrayHomo[Impl2[false], Impl2[true]]");
roundTrip(UsesStructArrayHomo.class, map("impls", Arrays.asList(map(), map("flag", true))), "UsesStructArrayHomo[Impl2[false], Impl2[true]]");
}

public static final class UsesStructArrayHomo {
Expand All @@ -285,9 +285,8 @@ public Impl2[] getImpls() {
}
}

@SuppressWarnings("unchecked")
@Test public void structListHomo() throws Exception {
roundTrip(UsesStructListHomo.class, map("impls", Arrays.asList(map("flag", false), map("flag", true))), "UsesStructListHomo[Impl2[false], Impl2[true]]");
roundTrip(UsesStructListHomo.class, map("impls", Arrays.asList(map(), map("flag", true))), "UsesStructListHomo[Impl2[false], Impl2[true]]");
}

public static final class UsesStructListHomo {
Expand All @@ -303,9 +302,8 @@ public List<Impl2> getImpls() {
}
}

@SuppressWarnings("unchecked")
@Test public void structCollectionHomo() throws Exception {
roundTrip(UsesStructCollectionHomo.class, map("impls", Arrays.asList(map("flag", false), map("flag", true))), "UsesStructCollectionHomo[Impl2[false], Impl2[true]]");
roundTrip(UsesStructCollectionHomo.class, map("impls", Arrays.asList(map(), map("flag", true))), "UsesStructCollectionHomo[Impl2[false], Impl2[true]]");
}

public static final class UsesStructCollectionHomo {
Expand All @@ -321,7 +319,6 @@ public Collection<Impl2> getImpls() {
}
}

@SuppressWarnings("unchecked")
@Test public void structArrayHetero() throws Exception {
roundTrip(UsesStructArrayHetero.class, map("bases", Arrays.asList(map("$class", "Impl1", "text", "hello"), map("$class", "Impl2", "flag", true))), "UsesStructArrayHetero[Impl1[hello], Impl2[true]]");
}
Expand All @@ -339,7 +336,6 @@ public Base[] getBases() {
}
}

@SuppressWarnings("unchecked")
@Test public void structListHetero() throws Exception {
roundTrip(UsesStructListHetero.class, map("bases", Arrays.asList(map("$class", "Impl1", "text", "hello"), map("$class", "Impl2", "flag", true))), "UsesStructListHetero[Impl1[hello], Impl2[true]]");
}
Expand All @@ -357,7 +353,6 @@ public List<Base> getBases() {
}
}

@SuppressWarnings("unchecked")
@Test public void structCollectionHetero() throws Exception {
roundTrip(UsesStructCollectionHetero.class, map("bases", Arrays.asList(map("$class", "Impl1", "text", "hello"), map("$class", "Impl2", "flag", true))), "UsesStructCollectionHetero[Impl1[hello], Impl2[true]]");
}
Expand All @@ -375,6 +370,66 @@ public Collection<Base> getBases() {
}
}

@Test public void defaultValuesStructCollectionCommon() throws Exception {
roundTrip(DefaultStructCollection.class, map("bases", Arrays.asList(map("$class", "Impl1", "text", "special"))), "DefaultStructCollection[Impl1[special]]");
}

@Test public void defaultValuesStructCollectionEmpty() throws Exception {
roundTrip(DefaultStructCollection.class, map("bases", Collections.emptyList()), "DefaultStructCollection[]");
}

@Issue("JENKINS-25779")
@Test public void defaultValuesStructCollection() throws Exception {
roundTrip(DefaultStructCollection.class, map(), "DefaultStructCollection[Impl1[default]]");
}

@Issue("JENKINS-25779")
@Test public void defaultValuesNestedStruct() throws Exception {
roundTrip(DefaultStructCollection.class, map("bases", Arrays.asList(map("$class", "Impl2"), map("$class", "Impl2", "flag", true))), "DefaultStructCollection[Impl2[false], Impl2[true]]");
}

@Issue("JENKINS-25779")
@Test public void defaultValuesNullSetter() throws Exception {
roundTrip(DefaultStructCollection.class, map("bases", null), "DefaultStructCollectionnull");
}

public static final class DefaultStructCollection {
private Collection<Base> bases = Arrays.<Base>asList(new Impl1("default"));
@DataBoundConstructor public DefaultStructCollection() {}
public Collection<Base> getBases() {return bases;}
@DataBoundSetter public void setBases(Collection<Base> bases) {this.bases = bases;}
@Override public String toString() {return "DefaultStructCollection" + bases;}
}

@Test public void defaultValuesStructArrayCommon() throws Exception {
roundTrip(DefaultStructArray.class, map("bases", Arrays.asList(map("$class", "Impl1", "text", "special")), "stuff", "val"), "DefaultStructArray[Impl1[special]];stuff=val");
}

@Issue("JENKINS-25779")
@Test public void defaultValuesStructArray() throws Exception {
roundTrip(DefaultStructArray.class, map("stuff", "val"), "DefaultStructArray[Impl1[default], Impl2[true]];stuff=val");
}

@Issue("JENKINS-25779")
@Test public void defaultValuesNullConstructorParameter() throws Exception {
roundTrip(DefaultStructArray.class, map(), "DefaultStructArray[Impl1[default], Impl2[true]];stuff=null");
}

public static final class DefaultStructArray {
private final String stuff;
private Base[] bases;
@DataBoundConstructor public DefaultStructArray(String stuff) {
this.stuff = stuff;
Impl2 impl2 = new Impl2();
impl2.setFlag(true);
bases = new Base[] {new Impl1("default"), impl2};
}
public Base[] getBases() {return bases;}
@DataBoundSetter public void setBases(Base[] bases) {this.bases = bases;}
public String getStuff() {return stuff;}
@Override public String toString() {return "DefaultStructArray" + Arrays.toString(bases) + ";stuff=" + stuff;}
}

private static Map<String,Object> map(Object... keysAndValues) {
if (keysAndValues.length % 2 != 0) {
throw new IllegalArgumentException();
Expand Down
@@ -0,0 +1,58 @@
/*
* The MIT License
*
* Copyright 2015 Jesse Glick.
*
* 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.jenkinsci.plugins.workflow.support.steps.input;

import java.util.Collections;
import org.jenkinsci.plugins.workflow.steps.StepConfigTester;
import org.jenkinsci.plugins.workflow.structs.DescribableHelper;
import org.junit.Test;
import static org.junit.Assert.*;
import org.junit.Rule;
import org.jvnet.hudson.test.Issue;
import org.jvnet.hudson.test.JenkinsRule;
import org.jvnet.hudson.test.WithoutJenkins;

public class InputStepTest {

@Rule public JenkinsRule r = new JenkinsRule();

@Test public void configRoundTrip() throws Exception {
InputStep s1 = new InputStep("hello world");
InputStep s2 = new StepConfigTester(r).configRoundTrip(s1);
assertEquals(s1.getMessage(), s2.getMessage());
assertEquals(s1.getId(), s2.getId());
assertEquals(s1.getParameters(), s2.getParameters());
assertEquals(s1.getOk(), s2.getOk());
assertEquals(s1.getSubmitter(), s2.getSubmitter());
}

@Issue("JENKINS-25779")
@WithoutJenkins
@Test public void uninstantiate() throws Exception {
InputStep s = new InputStep("hello world");
assertEquals(Collections.singletonMap("message", s.getMessage()), DescribableHelper.uninstantiate(s));
}

}

0 comments on commit 1714a18

Please sign in to comment.