org.jenkinsci.plugins.structs.describable.DescribableModelTest.java Source code

Java tutorial

Introduction

Here is the source code for org.jenkinsci.plugins.structs.describable.DescribableModelTest.java

Source

/*
 * The MIT License
 *
 * Copyright 2014 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.structs.describable;

import com.google.common.collect.ImmutableMap;
import hudson.Extension;
import hudson.model.AbstractDescribableImpl;
import hudson.model.BooleanParameterValue;
import hudson.model.Describable;
import hudson.model.Descriptor;
import hudson.model.ParameterValue;
import hudson.model.ParametersDefinitionProperty;
import hudson.model.Result;
import hudson.plugins.git.GitSCM;
import hudson.plugins.git.UserMergeOptions;
import hudson.plugins.git.extensions.impl.CleanBeforeCheckout;
import java.util.HashMap;
import org.codehaus.groovy.runtime.GStringImpl;
import org.hamcrest.Matchers;
import org.jenkinsci.plugins.structs.Fishing;
import org.jenkinsci.plugins.structs.FishingNet;
import org.jenkinsci.plugins.structs.Internet;
import org.jenkinsci.plugins.structs.Tech;
import org.jenkinsci.plugins.structs.describable.first.SharedName;
import org.junit.ClassRule;
import org.junit.Test;
import org.jvnet.hudson.test.Issue;
import org.jvnet.hudson.test.JenkinsRule;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.DataBoundSetter;

import java.net.URL;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.logging.Level;

import static org.apache.commons.lang3.SerializationUtils.roundtrip;
import static org.hamcrest.Matchers.instanceOf;
import static org.jenkinsci.plugins.structs.describable.DescribableModel.*;
import static org.jenkinsci.plugins.structs.describable.UninstantiatedDescribable.ANONYMOUS_KEY;
import static org.junit.Assert.*;
import org.jvnet.hudson.test.LoggerRule;

@SuppressWarnings("unchecked") // generic array construction
public class DescribableModelTest {
    @ClassRule
    public static JenkinsRule rule = new JenkinsRule();
    @ClassRule
    public static LoggerRule logging = new LoggerRule().record(DescribableModel.class, Level.ALL);

    @Test
    public void instantiate() throws Exception {
        Map<String, Object> args = map("text", "hello", "flag", true, "ignored", "!");
        assertEquals("C:hello/true", instantiate(C.class, args).toString());
        args.put("value", "main");
        assertEquals("I:main/hello/true", instantiate(I.class, args).toString());
        assertEquals("C:goodbye/false", instantiate(C.class, map("text", "goodbye")).toString());
    }

    private <T> T instantiate(Class<T> type, Map<String, Object> args) throws Exception {
        return new DescribableModel<T>(type).instantiate(args);
    }

    @Test
    public void uninstantiate() throws Exception {
        assertEquals("{flag=true, text=stuff}", DescribableModel.uninstantiate_(new C("stuff", true)).toString());
        I i = new I("stuff");
        i.setFlag(true);
        i.text = "more";
        assertEquals("{flag=true, text=more, value=stuff}", DescribableModel.uninstantiate_(i).toString());

        Object net = new Internet();
        UninstantiatedDescribable ud = UninstantiatedDescribable.from(net);
        assertEquals("net", ud.getSymbol());
        assertTrue(ud.getArguments().isEmpty());
        assertTrue(ud.instantiate(Tech.class) instanceof Internet);
    }

    @Test
    public void mismatchedTypes() throws Exception {
        try {
            instantiate(I.class, map("value", 99));
            fail();
        } catch (Exception x) {
            String message = x.getMessage();
            assertTrue(message, message.contains(I.class.getName()));
            assertTrue(message, message.contains("value"));
            assertTrue(message, message.contains("java.lang.String"));
            assertTrue(message, message.contains("java.lang.Integer"));
        }
    }

    @Test
    public void schemaFor() throws Exception {
        schema(C.class, "C(text: String, flag: boolean, shorty?: short, toBeRemoved?(deprecated): String)");
        schema(I.class, "I(value: String, flag?: boolean, text?: String)");
        DescribableModel<?> schema = new DescribableModel(Impl1.class);
        assertEquals("Implementation #1", schema.getDisplayName());
        assertEquals("<div>Overall help.</div>", schema.getHelp());
        assertEquals("<div>The text to display.</div>", schema.getParameter("text").getHelp());
        schema = new DescribableModel<C>(C.class);
        assertEquals("C", schema.getDisplayName());
        assertNull(schema.getHelp());
        assertNull(schema.getParameter("text").getHelp());
        assertFalse(schema.getParameter("text").isDeprecated());
        assertTrue(schema.getParameter("toBeRemoved").isDeprecated());
    }

    public static final class C {
        public final String text;
        private final boolean flag;
        private String toBeRemoved;

        @DataBoundConstructor
        public C(String text, boolean flag) {
            this.text = text;
            this.flag = flag;
        }

        public boolean isFlag() {
            return flag;
        }

        @Override
        public String toString() {
            return "C:" + text + "/" + flag;
        }

        // Are not actually trying to inject it; just making sure that unhandled @DataBoundSetter types are ignored if unused.
        public short getShorty() {
            return 0;
        }

        @DataBoundSetter
        public void setShorty(short s) {
            throw new UnsupportedOperationException();
        }

        public String getToBeRemoved() {
            return toBeRemoved;
        }

        @Deprecated
        @DataBoundSetter
        public void setToBeRemoved(String toBeRemoved) {
            this.toBeRemoved = toBeRemoved;
        }
    }

    public static final class I {
        private final String value;
        @DataBoundSetter
        private String text;
        private boolean flag;

        @DataBoundConstructor
        public I(String value) {
            this.value = value;
        }

        public String getValue() {
            return value;
        }

        public String getText() {
            return text;
        }

        public boolean isFlag() {
            return flag;
        }

        @DataBoundSetter
        public void setFlag(boolean f) {
            this.flag = f;
        }

        @Override
        public String toString() {
            return "I:" + value + "/" + text + "/" + flag;
        }
    }

    @Test
    public void findSubtypes() throws Exception {
        assertEquals(new HashSet<Class<?>>(Arrays.asList(Impl1.class, Impl2.class, Impl3.class, Impl4.class)),
                DescribableModel.findSubtypes(Base.class));
        assertEquals(Collections.singleton(Impl1.class), DescribableModel.findSubtypes(Marker.class));
    }

    @Test
    public void bindMapsFQN() throws Exception {
        assertEquals("UsesBase[Impl1[hello]]",
                instantiate(UsesBase.class, map("base", map(CLAZZ, Impl1.class.getName(), "text", "hello")))
                        .toString());
    }

    // TODO also check case that a FQN is needed

    @Test
    public void gstring() throws Exception {
        assertEquals("UsesBase[Impl1[hello world]]",
                instantiate(UsesBase.class,
                        map("base", map(CLAZZ, "Impl1", "text",
                                new GStringImpl(new Object[] { "hello", "world" }, new String[] { "", " " }))))
                                        .toString());
    }

    @Test
    public void nestedStructs() throws Exception {
        roundTrip(UsesBase.class, map("base", map(CLAZZ, "Impl1", "text", "hello")));
        roundTrip(UsesBase.class, map("base", map(CLAZZ, "Impl2", "flag", true)));
        roundTrip(UsesImpl2.class, map("impl2", map()));
        schema(UsesBase.class,
                "UsesBase(base: Base{Impl1(text: String) | Impl2(flag?: boolean) | Impl3(base: Base) | Impl4(bases: Base[])})");
        schema(UsesImpl2.class, "UsesImpl2(impl2: Impl2(flag?: boolean))");
        schema(UsesUnimplementedExtensionPoint.class,
                "UsesUnimplementedExtensionPoint(delegate: UnimplementedExtensionPoint{})");
        schema(UsesSomeImplsBroken.class, "UsesSomeImplsBroken(delegate: SomeImplsBroken{FineImpl()})");
    }

    public static class UsesBase {
        public final Base base;

        @DataBoundConstructor
        public UsesBase(Base base) {
            this.base = base;
        }

        @Override
        public String toString() {
            return "UsesBase[" + base + "]";
        }
    }

    public static class UsesImpl2 {
        public final Impl2 impl2;

        @DataBoundConstructor
        public UsesImpl2(Impl2 impl2) {
            this.impl2 = impl2;
        }

        @Override
        public String toString() {
            return "UsesImpl2[" + impl2 + "]";
        }
    }

    public static abstract class Base extends AbstractDescribableImpl<Base> {
    }

    public interface Marker {
    }

    public static final class Impl1 extends Base implements Marker {
        private final String text;

        @DataBoundConstructor
        public Impl1(String text) {
            this.text = text;
        }

        public String getText() {
            return text;
        }

        @Override
        public String toString() {
            return "Impl1[" + text + "]";
        }

        @Extension
        public static final class DescriptorImpl extends Descriptor<Base> {
            @Override
            public String getDisplayName() {
                return "Implementation #1";
            }
        }
    }

    public static final class Impl2 extends Base {
        private boolean flag;

        @DataBoundConstructor
        public Impl2() {
        }

        public boolean isFlag() {
            return flag;
        }

        @DataBoundSetter
        public void setFlag(boolean flag) {
            this.flag = flag;
        }

        @Override
        public String toString() {
            return "Impl2[" + flag + "]";
        }

        @Extension
        public static final class DescriptorImpl extends Descriptor<Base> {
            @Override
            public String getDisplayName() {
                return "Impl2";
            }
        }
    }

    //use to trigger recursion subcases
    public static final class Impl3 extends Base {
        private final Base base;

        @DataBoundConstructor
        public Impl3(Base base) {
            this.base = base;
        }

        public Base getBase() {
            return base;
        }

        @Override
        public String toString() {
            return "Impl3[" + base.toString() + "]";
        }

        @Extension
        public static final class DescriptorImpl extends Descriptor<Base> {
            @Override
            public String getDisplayName() {
                return "Impl3";
            }
        }
    }

    public static final class Impl4 extends Base {
        private final Base[] bases;

        @DataBoundConstructor
        public Impl4(Base[] bases) {
            this.bases = bases;
        }

        public Base[] getBases() {
            return bases;
        }

        @Override
        public String toString() {
            return "Impl4[" + Arrays.toString(bases) + "]";
        }

        @Extension
        public static final class DescriptorImpl extends Descriptor<Base> {
            @Override
            public String getDisplayName() {
                return "Impl4";
            }
        }
    }

    public static abstract class UnimplementedExtensionPoint
            extends AbstractDescribableImpl<UnimplementedExtensionPoint> {
    }

    public static final class UsesUnimplementedExtensionPoint {
        @DataBoundConstructor
        public UsesUnimplementedExtensionPoint(UnimplementedExtensionPoint delegate) {
        }
    }

    public static abstract class SomeImplsBroken extends AbstractDescribableImpl<SomeImplsBroken> {
    }

    public static class BrokenImpl extends SomeImplsBroken {
        @Extension
        public static class DescriptorImpl extends Descriptor<SomeImplsBroken> {
            @Override
            public String getDisplayName() {
                return "BrokenImpl";
            }
        }
    }

    public static class FineImpl extends SomeImplsBroken {
        @DataBoundConstructor
        public FineImpl() {
        }

        @Extension
        public static class DescriptorImpl extends Descriptor<SomeImplsBroken> {
            @Override
            public String getDisplayName() {
                return "FineImpl";
            }
        }
    }

    public static class UsesSomeImplsBroken {
        @DataBoundConstructor
        public UsesSomeImplsBroken(SomeImplsBroken delegate) {
        }
    }

    @Test
    public void enums() throws Exception {
        roundTrip(UsesEnum.class, map("e", "ZERO"));
        schema(UsesEnum.class, "UsesEnum(e: E[ZERO])");
    }

    @Test
    public void enumsWithGString() throws Exception {
        assertEquals("UsesEnum[ZERO]",
                instantiate(UsesEnum.class, map("e", new GStringImpl(new Object[0], new String[] { "ZERO" })))
                        .toString());
    }

    public static final class UsesEnum {
        private final E e;

        @DataBoundConstructor
        public UsesEnum(E e) {
            this.e = e;
        }

        public E getE() {
            return e;
        }

        @Override
        public String toString() {
            return "UsesEnum[" + e + "]";
        }
    }

    public enum E {
        ZERO() {
            @Override
            public int v() {
                return 0;
            }
        };
        public abstract int v();
    }

    @Test
    public void urls() throws Exception {
        roundTrip(UsesURL.class, map("u", "http://nowhere.net/"));
        schema(UsesURL.class, "UsesURL(u?: String)");
    }

    @Test
    public void urlsWithGString() throws Exception {
        assertEquals("UsesURL[http://nowhere.net/]",
                instantiate(UsesURL.class,
                        map("u", new GStringImpl(new Object[0], new String[] { "http://nowhere.net/" })))
                                .toString());
    }

    public static final class UsesURL {
        @DataBoundConstructor
        public UsesURL() {
        }

        @DataBoundSetter
        public URL u;

        @Override
        public String toString() {
            return "UsesURL[" + u + "]";
        }
    }

    @Test
    public void result() throws Exception {
        roundTrip(UsesResult.class, map("r", "SUCCESS"));
        schema(UsesResult.class, "UsesResult(r?: String)");
    }

    public static final class UsesResult {
        @DataBoundConstructor
        public UsesResult() {
        }

        @DataBoundSetter
        public Result r;

        @Override
        public String toString() {
            return "UsesResult[" + r + "]";
        }
    }

    @Test
    public void chars() throws Exception {
        roundTrip(UsesCharacter.class, map("c", "!"));
        schema(UsesCharacter.class, "UsesCharacter(c?: char)");
    }

    @Test
    public void charsWithGString() throws Exception {
        assertEquals("UsesCharacter[x]",
                instantiate(UsesCharacter.class, map("c", new GStringImpl(new Object[0], new String[] { "x" })))
                        .toString());
    }

    public static final class UsesCharacter {
        @DataBoundConstructor
        public UsesCharacter() {
        }

        @DataBoundSetter
        public char c;

        @Override
        public String toString() {
            return "UsesCharacter[" + c + "]";
        }
    }

    @Test
    public void stringArray() throws Exception {
        roundTrip(UsesStringArray.class, map("strings", Arrays.asList("one", "two")));
        schema(UsesStringArray.class, "UsesStringArray(strings: String[])");
    }

    @Test
    public void stringList() throws Exception {
        roundTrip(UsesStringList.class, map("strings", Arrays.asList("one", "two")));
        schema(UsesStringList.class, "UsesStringList(strings: String[])");
    }

    public static final class UsesStringArray {
        private final String[] strings;

        @DataBoundConstructor
        public UsesStringArray(String[] strings) {
            this.strings = strings;
        }

        public String[] getStrings() {
            return strings;
        }
    }

    public static final class UsesStringList {
        private final List<String> strings;

        @DataBoundConstructor
        public UsesStringList(List<String> strings) {
            this.strings = strings;
        }

        public List<String> getStrings() {
            return strings;
        }
    }

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

    public static final class UsesStructArrayHomo {
        private final Impl2[] impls;

        @DataBoundConstructor
        public UsesStructArrayHomo(Impl2[] impls) {
            this.impls = impls;
        }

        public Impl2[] getImpls() {
            return impls;
        }

        @Override
        public String toString() {
            return "UsesStructArrayHomo" + Arrays.toString(impls);
        }
    }

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

    public static final class UsesStructListHomo {
        private final List<Impl2> impls;

        @DataBoundConstructor
        public UsesStructListHomo(List<Impl2> impls) {
            this.impls = impls;
        }

        public List<Impl2> getImpls() {
            return impls;
        }

        @Override
        public String toString() {
            return "UsesStructListHomo" + impls;
        }
    }

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

    public static final class UsesStructCollectionHomo {
        private final Collection<Impl2> impls;

        @DataBoundConstructor
        public UsesStructCollectionHomo(Collection<Impl2> impls) {
            this.impls = impls;
        }

        public Collection<Impl2> getImpls() {
            return impls;
        }

        @Override
        public String toString() {
            return "UsesStructCollectionHomo" + impls;
        }
    }

    @Test
    public void structArrayHetero() throws Exception {
        roundTrip(UsesStructArrayHetero.class,
                map("bases",
                        Arrays.asList(map(CLAZZ, "Impl1", "text", "hello"), map(CLAZZ, "Impl2", "flag", true))),
                "UsesStructArrayHetero[Impl1[hello], Impl2[true]]");
        schema(UsesStructArrayHetero.class,
                "UsesStructArrayHetero(bases: Base{Impl1(text: String) | Impl2(flag?: boolean) | Impl3(base: Base) | Impl4(bases: Base[])}[])");
    }

    public static final class UsesStructArrayHetero {
        private final Base[] bases;

        @DataBoundConstructor
        public UsesStructArrayHetero(Base[] bases) {
            this.bases = bases;
        }

        public Base[] getBases() {
            return bases;
        }

        @Override
        public String toString() {
            return "UsesStructArrayHetero" + Arrays.toString(bases);
        }
    }

    @Test
    public void structListHetero() throws Exception {
        roundTrip(UsesStructListHetero.class,
                map("bases",
                        Arrays.asList(map(CLAZZ, "Impl1", "text", "hello"), map(CLAZZ, "Impl2", "flag", true))),
                "UsesStructListHetero[Impl1[hello], Impl2[true]]");
        schema(UsesStructListHetero.class,
                "UsesStructListHetero(bases: Base{Impl1(text: String) | Impl2(flag?: boolean) | Impl3(base: Base) | Impl4(bases: Base[])}[])");
    }

    public static final class UsesStructListHetero {
        private final List<Base> bases;

        @DataBoundConstructor
        public UsesStructListHetero(List<Base> bases) {
            this.bases = bases;
        }

        public List<Base> getBases() {
            return bases;
        }

        @Override
        public String toString() {
            return "UsesStructListHetero" + bases;
        }
    }

    @Test
    public void structCollectionHetero() throws Exception {
        roundTrip(UsesStructCollectionHetero.class,
                map("bases",
                        Arrays.asList(map(CLAZZ, "Impl1", "text", "hello"), map(CLAZZ, "Impl2", "flag", true))),
                "UsesStructCollectionHetero[Impl1[hello], Impl2[true]]");
        schema(UsesStructCollectionHetero.class,
                "UsesStructCollectionHetero(bases: Base{Impl1(text: String) | Impl2(flag?: boolean) | Impl3(base: Base) | Impl4(bases: Base[])}[])");
    }

    public static final class UsesStructCollectionHetero {
        private final Collection<Base> bases;

        @DataBoundConstructor
        public UsesStructCollectionHetero(Collection<Base> bases) {
            this.bases = bases;
        }

        public Collection<Base> getBases() {
            return bases;
        }

        @Override
        public String toString() {
            return "UsesStructCollectionHetero" + bases;
        }
    }

    @Test
    public void defaultValuesStructCollectionCommon() throws Exception {
        roundTrip(DefaultStructCollection.class,
                map("bases", Arrays.asList(map(CLAZZ, "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(CLAZZ, "Impl2"), map(CLAZZ, "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(CLAZZ, "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;
        }
    }

    @Issue("JENKINS-26093")
    @Test
    public void parameterValues() throws Exception {
        assertTrue(DescribableModel.findSubtypes(ParameterValue.class).contains(BooleanParameterValue.class)); //  do not want to enumerate ListSubversionTagsParameterValue etc.
        // Omitting RunParameterValue since it is not friendly for a unit test.
        // JobParameterDefinition is not registered as an extension, so not supporting FileParameterValue.
        // FileParameterValue is unsupportable as an input to a WorkflowJob since it requires createBuildWrapper to work;
        // as an argument to BuildTriggerStep it could perhaps work, but we would need to provide a custom FileItem implementation.
        // PasswordParameterValue requires Secret.fromString and thus JenkinsRule.
        // For others: https://github.com/search?type=Code&q=user%3Ajenkinsci+user%3Acloudbees+%22extends+ParameterDefinition%22
        roundTrip(TakesParams.class,
                map("parameters",
                        Arrays.asList(map(CLAZZ, "BooleanParameterValue", "name", "flag", "value", true),
                                map(CLAZZ, "StringParameterValue", "name", "n", "value", "stuff"),
                                map(CLAZZ, "TextParameterValue", "name", "text", "value", "here\nthere"))),
                "TakesParams;BooleanParameterValue:flag=true;StringParameterValue:n=stuff;TextParameterValue:text=here\nthere");
        assertEquals("(parameters=[@booleanParam$BooleanParameterValue(name=flag,value=true)])",
                DescribableModel
                        .uninstantiate2_(new TakesParams(
                                Collections.<ParameterValue>singletonList(new BooleanParameterValue("flag", true))))
                        .toString());
    }

    public static final class TakesParams {
        public final List<ParameterValue> parameters;

        @DataBoundConstructor
        public TakesParams(List<ParameterValue> parameters) {
            this.parameters = parameters;
        }

        @Override
        public String toString() {
            StringBuilder b = new StringBuilder("TakesParams");
            for (ParameterValue v : parameters) {
                b.append(';').append(v.getClass().getSimpleName()).append(':').append(v.getShortDescription());
            }
            return b.toString();
        }
    }

    @Test
    public void parametersDefinitionProperty() throws Exception {
        roundTrip(ParametersDefinitionProperty.class,
                map("parameterDefinitions",
                        Arrays.asList(
                                map(CLAZZ, "BooleanParameterDefinition", "name", "flag", "defaultValue", false),
                                map(CLAZZ, "StringParameterDefinition", "name", "text"))));
    }

    @Issue("JENKINS-26619")
    @Test
    public void getterDescribableList() throws Exception {
        roundTrip(GitSCM.class,
                map("extensions", Arrays.asList(map(CLAZZ, CleanBeforeCheckout.class.getSimpleName())),
                        // Default values for these things do not work because GitSCM fails to use @DataBoundSetter:
                        "branches", Arrays.asList(map("name", "*/master")), "doGenerateSubmoduleConfigurations",
                        false, "submoduleCfg", Collections.emptyList(), "userRemoteConfigs",
                        Collections.emptyList()));
    }

    @Issue("JENKINS-34070")
    @Test
    public void userMergeOptions() throws Exception {
        roundTrip(UserMergeOptions.class,
                map("mergeRemote", "x", "mergeTarget", "y", "mergeStrategy", "OCTOPUS", "fastForwardMode",
                        "FF_ONLY"),
                "UserMergeOptions{mergeRemote='x', mergeTarget='y', mergeStrategy='OCTOPUS', fastForwardMode='FF_ONLY'}");
    }

    @Issue("JENKINS-32925") // but Base3/Base4 usages are the more realistic case
    @Test
    public void recursion() throws Exception {
        schema(Recursion.class, "Recursion(foo?: Recursion)");
    }

    public static class Recursion {
        @DataBoundConstructor
        public Recursion() {
        }

        @DataBoundSetter
        public void setFoo(Recursion r) {
        }
    }

    /**
     * Makes sure resolveClass can do both symbol & class name lookup
     */
    @Test
    public void resolveClass() throws Exception {
        assertEquals(FishingNet.class, DescribableModel.resolveClass(Fishing.class, null, "net"));
        assertEquals(FishingNet.class, DescribableModel.resolveClass(Fishing.class, "FishingNet", null));
        assertEquals(Internet.class, DescribableModel.resolveClass(Tech.class, null, "net"));
        assertEquals(Internet.class, DescribableModel.resolveClass(Tech.class, "Internet", null));
    }

    @Issue("JENKINS-46122")
    @Test
    public void resolveSymbolOnWrongBaseClass() throws Exception {
        try {
            DescribableModel.resolveClass(Tech.class, null, "rod");
            fail("No symbol for Tech should exist.");
        } catch (UnsupportedOperationException e) {
            assertEquals("no known implementation of " + Tech.class + " is using symbol rod", e.getMessage());
        }
    }

    @Test
    public void singleRequiredParameter() throws Exception {
        // positive case
        DescribableModel dc = new DescribableModel(LoneStar.class);
        assertTrue(dc.hasSingleRequiredParameter());
        assertEquals("star", dc.getSoleRequiredParameter().getName());
        assertNotNull(dc.getParameter("capital"));

        // negative case
        dc = new DescribableModel(ThreeStars.class);
        assertFalse(dc.hasSingleRequiredParameter());

        dc = new DescribableModel(LoneStar.class);
        UninstantiatedDescribable x = dc.uninstantiate2(new LoneStar("foo"));
        assertTrue(x.hasSoleRequiredArgument());

        LoneStar star = new LoneStar("foo");
        star.setCapital("Should be Dallas");
        UninstantiatedDescribable y = dc.uninstantiate2(star);
        assertTrue(!y.hasSoleRequiredArgument());
    }

    @Test
    public void anonymousKey() throws Exception {
        DescribableModel dc = new DescribableModel(LoneStar.class);
        UninstantiatedDescribable u = dc.uninstantiate2(new LoneStar("texas"));
        assertFalse(u.getArguments().containsKey(ANONYMOUS_KEY)); // shouldn't show up as a key from uninstantiate
        assertEquals("texas", u.getSymbol());

        // but the key can be used during construction
        LoneStar ls = (LoneStar) new UninstantiatedDescribable("texas", null,
                Collections.singletonMap(ANONYMOUS_KEY, "alamo")).instantiate();
        assertEquals("alamo", ls.star);

        // it cannot be used when multiple parameters are given
        try {
            new UninstantiatedDescribable("texas", null,
                    ImmutableMap.of(ANONYMOUS_KEY, "alamo", "capital", "Austin")).instantiate();
            fail();
        } catch (IllegalArgumentException e) {
            // expected
        }
    }

    @Test
    public void coerceNumbersAndBoolean() throws Exception {
        IntAndBool intAndBool = (IntAndBool) new UninstantiatedDescribable("intAndBool", null,
                ImmutableMap.<String, Object>of("i", "5", "b", "true")).instantiate();
        assertEquals(5, intAndBool.i);
        assertEquals(true, intAndBool.b);
    }

    @Test
    public void serialization() {
        LoneStar s = new LoneStar("texas");
        DescribableModel<LoneStar> m = DescribableModel.of(LoneStar.class);
        UninstantiatedDescribable d = m.uninstantiate2(s);
        assertSame(d.getModel(), m);
        d = roundtrip(d);
        assertNotNull(d.getModel());
        assertEquals("texas", d.getSymbol());
    }

    @Test
    public void deprecated() throws Exception {
        assertTrue(new DescribableModel(ToBeRemoved.class).isDeprecated());
        assertFalse(new DescribableModel(Impl1.class).isDeprecated());
    }

    @Deprecated
    public static class ToBeRemoved {
        @DataBoundConstructor
        public ToBeRemoved() {
        }
    }

    @Issue("JENKINS-43337")
    @Test
    public void ambiguousSimpleName() throws Exception {
        AmbiguousContainer container = new AmbiguousContainer(new FirstAmbiguous.CommonName("first"),
                new UnambiguousClassName("second"));

        UninstantiatedDescribable ud = DescribableModel.uninstantiate2_(container);

        Object o = ud.toMap().get("ambiguous");
        assertTrue(o instanceof Map);
        Map<String, Object> m = (Map<String, Object>) o;

        // Make sure the ambiguous class is fully qualified.
        assertEquals(FirstAmbiguous.CommonName.class.getName(), m.get("$class"));

        Object o2 = ud.toMap().get("unambiguous");
        assertTrue(o2 instanceof Map);
        Map<String, Object> m2 = (Map<String, Object>) o2;

        // Make sure the unambiguous class just uses the simple name.
        assertEquals(UnambiguousClassName.class.getSimpleName(), m2.get("$class"));
    }

    @Issue("JENKINS-45130") //
    @Test
    public void ambiguousTopLevelSimpleName() throws Exception {
        AmbiguousContainer container = new AmbiguousContainer(new SharedName("first"),
                new UnambiguousClassName("second"));

        UninstantiatedDescribable ud = DescribableModel.uninstantiate2_(container);

        Object o = ud.toMap().get("ambiguous");
        assertTrue(o instanceof Map);
        Map<String, Object> m = (Map<String, Object>) o;

        // Make sure the ambiguous class is fully qualified.
        assertEquals(SharedName.class.getName(), m.get("$class"));

        Object o2 = ud.toMap().get("unambiguous");
        assertTrue(o2 instanceof Map);
        Map<String, Object> m2 = (Map<String, Object>) o2;

        // Make sure the unambiguous class just uses the simple name.
        assertEquals(UnambiguousClassName.class.getSimpleName(), m2.get("$class"));
    }

    @Issue("JENKINS-45130")
    @Test
    public void ambiguousTopLevelSimpleNameInList() throws Exception {
        SharedName first = new SharedName("first");
        first.setTwo("something");
        AmbiguousListContainer container = new AmbiguousListContainer(
                Arrays.<Describable<?>>asList(first, new UnambiguousClassName("second")));

        UninstantiatedDescribable ud = DescribableModel.uninstantiate2_(container);

        Object o = ud.toMap().get("list");
        assertTrue(o instanceof List);
        List<Map<String, Object>> l = (List<Map<String, Object>>) o;

        Map<String, Object> m = l.get(0);

        // Make sure the ambiguous class is fully qualified.
        assertEquals(SharedName.class.getName(), m.get("$class"));

        Map<String, Object> m2 = l.get(1);

        // Make sure the unambiguous class just uses the simple name.
        assertEquals(UnambiguousClassName.class.getSimpleName(), m2.get("$class"));

        System.out.println(ud.toString());

        AmbiguousListContainer roundtrip = (AmbiguousListContainer) ud.instantiate();
        assertThat(roundtrip.list.get(0), instanceOf(SharedName.class));
        assertThat(roundtrip.list.get(1), instanceOf(UnambiguousClassName.class));
    }

    @Issue("JENKINS-45130")
    @Test
    public void ambiguousTopLevelSimpleNameInArray() throws Exception {
        SharedName first = new SharedName("first");
        first.setTwo("something");
        AmbiguousArrayContainer container = new AmbiguousArrayContainer(first, new UnambiguousClassName("second"));

        UninstantiatedDescribable ud = DescribableModel.uninstantiate2_(container);

        Object o = ud.toMap().get("array");
        assertTrue(o instanceof List);
        List<Map<String, Object>> l = (List<Map<String, Object>>) o;

        Map<String, Object> m = l.get(0);

        // Make sure the ambiguous class is fully qualified.
        assertEquals(SharedName.class.getName(), m.get("$class"));

        Map<String, Object> m2 = l.get(1);

        // Make sure the unambiguous class just uses the simple name.
        assertEquals(UnambiguousClassName.class.getSimpleName(), m2.get("$class"));

        System.out.println(ud.toString());

        AmbiguousArrayContainer roundtrip = (AmbiguousArrayContainer) ud.instantiate();
        assertThat(roundtrip.getArray()[0], instanceOf(SharedName.class));
        assertThat(roundtrip.getArray()[1], instanceOf(UnambiguousClassName.class));
    }

    private static Map<String, Object> map(Object... keysAndValues) {
        if (keysAndValues.length % 2 != 0) {
            throw new IllegalArgumentException();
        }
        Map<String, Object> m = new TreeMap<String, Object>();
        for (int i = 0; i < keysAndValues.length; i += 2) {
            m.put((String) keysAndValues[i], keysAndValues[i + 1]);
        }
        return m;
    }

    private void roundTrip(Class<?> c, Map<String, Object> m) throws Exception {
        roundTrip(c, m, null);
    }

    private void roundTrip(Class<?> c, Map<String, Object> m, String toString) throws Exception {
        Object o = instantiate(c, m);
        if (toString != null) {
            assertEquals(toString, o.toString());
        }
        Map<String, Object> m2 = DescribableModel.uninstantiate_(o);
        assertEquals(m, m2);
    }

    private static void schema(Class<?> c, String schema) throws Exception {
        assertEquals(schema, new DescribableModel(c).toString());
    }

    public static final class AllJavaStandardTypesClass {
        // final values (set in constructor)
        private final boolean booleanValue1;
        private final byte byteValue1;
        private final short shortValue1;
        private final int intValue1;
        private final long longValue1;
        private final float floatValue1;
        private final double doubleValue1;

        @DataBoundConstructor
        public AllJavaStandardTypesClass(boolean booleanValue1, byte byteValue1, short shortValue1, int intValue1,
                long longValue1, float floatValue1, double doubleValue1) {
            this.booleanValue1 = booleanValue1;
            this.byteValue1 = byteValue1;
            this.shortValue1 = shortValue1;
            this.intValue1 = intValue1;
            this.longValue1 = longValue1;
            this.floatValue1 = floatValue1;
            this.doubleValue1 = doubleValue1;
        }

        public boolean isBooleanValue1() {
            return booleanValue1;
        }

        public byte getByteValue1() {
            return byteValue1;
        }

        public short getShortValue1() {
            return shortValue1;
        }

        public int getIntValue1() {
            return intValue1;
        }

        public long getLongValue1() {
            return longValue1;
        }

        public float getFloatValue1() {
            return floatValue1;
        }

        public double getDoubleValue1() {
            return doubleValue1;
        }

        @Override
        public String toString() {
            return "AllJavaStandardTypesClass{" + "" + booleanValue1 + "," + byteValue1 + "," + shortValue1 + ","
                    + intValue1 + "," + longValue1 + "," + floatValue1 + "," + doubleValue1 + '}';
        }
    }

    @Issue("JENKINS-31967")
    @Test
    public void testJavaStandardTypes() throws Exception {
        // check instantiate with not default values
        roundTrip(AllJavaStandardTypesClass.class,
                map("booleanValue1", Boolean.TRUE, "byteValue1", Byte.MAX_VALUE, "shortValue1", Short.MAX_VALUE,
                        "intValue1", Integer.MAX_VALUE, "longValue1", Long.MAX_VALUE, "floatValue1",
                        Float.MAX_VALUE, "doubleValue1", Double.MAX_VALUE));
        // check with default values
        roundTrip(AllJavaStandardTypesClass.class,
                map("booleanValue1", false, "byteValue1", (byte) 0, "shortValue1", (short) 0, "intValue1", 0,
                        "longValue1", (long) 0, "floatValue1", (float) 0.0, "doubleValue1", 0.0));
    }

    @Test
    @Issue("JENKINS-44864")
    public void given_model_when_fieldRenamed_then_uselessSettersIgnored() throws Exception {
        EvolvedClass instance;
        Map<String, Object> expected = new HashMap<String, Object>();

        instance = new EvolvedClass(false);
        expected.clear();
        expected.put("option", Boolean.FALSE);
        assertThat("Only required properties", DescribableModel.uninstantiate2_(instance).toMap(),
                Matchers.is(expected));

        instance = new EvolvedClass(true);
        expected.clear();
        expected.put("option", Boolean.TRUE);
        assertThat("Only required properties", DescribableModel.uninstantiate2_(instance).toMap(),
                Matchers.is(expected));

        instance = new EvolvedClass(true);
        instance.setName("bob");
        expected.clear();
        expected.put("option", Boolean.TRUE);
        expected.put("name", "bob");
        assertThat("current setters are rendered", DescribableModel.uninstantiate2_(instance).toMap(),
                Matchers.is(expected));

        instance = new EvolvedClass(false);
        instance.setTitle("bill");
        expected.clear();
        expected.put("option", Boolean.FALSE);
        expected.put("name", "bill");
        assertThat("migrated setters are migrated", DescribableModel.uninstantiate2_(instance).toMap(),
                Matchers.is(expected));

        instance = new EvolvedClass(false);
        instance.setName("random");
        instance.setDescription("This is the thing");
        expected.clear();
        expected.put("option", Boolean.FALSE);
        expected.put("name", "random");
        expected.put("description", "This is the thing");
        assertThat("if the old setter mangles the value but has 1:1 mapping, we still ignore redundant setters",
                DescribableModel.uninstantiate2_(instance).toMap(), Matchers.is(expected));

        instance = new EvolvedClass(false);
        instance.setName("random");
        instance.setSummary("Legacy summary");
        expected.clear();
        expected.put("option", Boolean.FALSE);
        expected.put("name", "random");
        expected.put("description", "Prefix: Legacy summary");
        assertThat("Doesn't matter if we set the value through the old setter or the new",
                DescribableModel.uninstantiate2_(instance).toMap(), Matchers.is(expected));

        instance = new EvolvedClass(false);
        instance.setLegacyMode(53);
        expected.clear();
        expected.put("option", Boolean.FALSE);
        expected.put("legacyMode", 53);
        assertThat("A deprecated setter that produces a different object instance is retained",
                DescribableModel.uninstantiate2_(instance).toMap(), Matchers.is(expected));

    }

    public static class EvolvedClass {
        private final boolean option;
        private String name;
        private String description;
        private Integer legacyMode;

        @DataBoundConstructor
        public EvolvedClass(boolean option) {
            this.option = option;
        }

        public boolean isOption() {
            return option;
        }

        public String getName() {
            return name;
        }

        @DataBoundSetter
        public void setName(String name) {
            this.name = name;
        }

        @Deprecated
        public String getTitle() {
            return name;
        }

        @Deprecated
        @DataBoundSetter
        public void setTitle(String title) {
            this.name = title;
        }

        public String getDescription() {
            return description;
        }

        @DataBoundSetter
        public void setDescription(String description) {
            this.description = description;
        }

        @Deprecated
        public String getSummary() {
            return description == null ? null
                    : (description.startsWith("Prefix: ") ? description.substring(8) : description);
        }

        @Deprecated
        @DataBoundSetter
        public void setSummary(String summary) {
            description = summary == null ? null : "Prefix: " + summary;
        }

        @Deprecated
        public Integer getLegacyMode() {
            return legacyMode;
        }

        @Deprecated
        @DataBoundSetter
        public void setLegacyMode(Integer legacyMode) {
            this.legacyMode = legacyMode;
        }
    }
}