/* * 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.workflow.cps; import groovy.lang.GroovyShell; import hudson.model.BooleanParameterDefinition; import hudson.model.BooleanParameterValue; import hudson.model.FreeStyleProject; import hudson.model.ParametersDefinitionProperty; import hudson.model.StringParameterDefinition; import hudson.model.StringParameterValue; import hudson.tasks.ArtifactArchiver; import org.jenkinsci.Symbol; import org.jenkinsci.plugins.structs.describable.DescribableModel; import org.jenkinsci.plugins.workflow.cps.steps.ParallelStep; import org.jenkinsci.plugins.workflow.steps.CatchErrorStep; import org.jenkinsci.plugins.workflow.steps.CoreStep; import org.jenkinsci.plugins.workflow.steps.EchoStep; import org.jenkinsci.plugins.workflow.steps.PwdStep; import org.jenkinsci.plugins.workflow.steps.ReadFileStep; import org.jenkinsci.plugins.workflow.support.steps.ExecutorStep; 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.jenkinsci.plugins.workflow.testMetaStep.Colorado; import org.jenkinsci.plugins.workflow.testMetaStep.Hawaii; import org.jenkinsci.plugins.workflow.testMetaStep.Island; import org.jenkinsci.plugins.workflow.testMetaStep.MonomorphicData; import org.jenkinsci.plugins.workflow.testMetaStep.MonomorphicDataWithSymbol; import org.jenkinsci.plugins.workflow.testMetaStep.MonomorphicListStep; import org.jenkinsci.plugins.workflow.testMetaStep.MonomorphicListWithSymbolStep; import org.jenkinsci.plugins.workflow.testMetaStep.MonomorphicStep; import org.jenkinsci.plugins.workflow.testMetaStep.MonomorphicWithSymbolStep; import org.jenkinsci.plugins.workflow.testMetaStep.Oregon; import org.jenkinsci.plugins.workflow.testMetaStep.StateMetaStep; import org.jenkinsci.plugins.workflow.testMetaStep.chemical.CarbonMonoxide; import org.jenkinsci.plugins.workflow.testMetaStep.chemical.DetectionMetaStep; import org.junit.ClassRule; import org.junit.Test; import org.jvnet.hudson.test.Email; import org.jvnet.hudson.test.Issue; import org.jvnet.hudson.test.JenkinsRule; import org.jvnet.hudson.test.MockFolder; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.logging.Level; import static org.hamcrest.CoreMatchers.*; import org.jenkinsci.plugins.workflow.testMetaStep.Circle; import org.jenkinsci.plugins.workflow.testMetaStep.CurveMetaStep; import org.jenkinsci.plugins.workflow.testMetaStep.Polygon; import static org.junit.Assert.*; import org.jvnet.hudson.test.LoggerRule; import org.kohsuke.stapler.NoStaplerConstructorException; // TODO these tests would better be moved to the respective plugins public class SnippetizerTest { @ClassRule public static JenkinsRule r = new JenkinsRule(); @ClassRule public static LoggerRule logging = new LoggerRule().record(DescribableModel.class, Level.ALL); private static SnippetizerTester st = new SnippetizerTester(r); @Test public void basics() throws Exception { st.assertRoundTrip(new EchoStep("hello world"), "echo 'hello world'"); ReadFileStep s = new ReadFileStep("build.properties"); st.assertRoundTrip(s, "readFile 'build.properties'"); s.setEncoding("ISO-8859-1"); st.assertRoundTrip(s, "readFile encoding: 'ISO-8859-1', file: 'build.properties'"); } @Email("https://groups.google.com/forum/#!topicsearchin/jenkinsci-users/workflow/jenkinsci-users/DJ15tkEQPw0") @Test public void noArgStep() throws Exception { st.assertRoundTrip(new PwdStep(), "pwd()"); } @Test public void coreStep() throws Exception { ArtifactArchiver aa = new ArtifactArchiver("x.jar"); aa.setAllowEmptyArchive(true); if (ArtifactArchiver.DescriptorImpl.class.isAnnotationPresent(Symbol.class)) { st.assertRoundTrip(new CoreStep(aa), "archiveArtifacts allowEmptyArchive: true, artifacts: 'x.jar'"); } else { // TODO 2.x delete st.assertRoundTrip(new CoreStep(aa), "step([$class: 'ArtifactArchiver', allowEmptyArchive: true, artifacts: 'x.jar'])"); } } @Test public void coreStep2() throws Exception { if (ArtifactArchiver.DescriptorImpl.class.isAnnotationPresent(Symbol.class)) { st.assertRoundTrip(new CoreStep(new ArtifactArchiver("x.jar")), "archiveArtifacts 'x.jar'"); } else { // TODO 2.x delete st.assertRoundTrip(new CoreStep(new ArtifactArchiver("x.jar")), "step([$class: 'ArtifactArchiver', artifacts: 'x.jar'])"); } } @Test public void recursiveSymbolUse() throws Exception { Island hawaii = new Island(new Island(new Island(),null),new Island()); st.assertRoundTrip(new StateMetaStep(new Hawaii(hawaii)), "hawaii island(lhs: island(lhs: island()), rhs: island())"); } @Test public void collisionWithStep() throws Exception { // this cannot use "or()" due to a collision with OrStep st.assertRoundTrip(new StateMetaStep(new Oregon()), "state([$class: 'Oregon'])"); } @Test public void collisionWithAnotherMetaStep() throws Exception { // neither should produce "CO()" because that would prevent disambiguation st.assertRoundTrip(new StateMetaStep(new Colorado()), "state CO()"); st.assertRoundTrip(new DetectionMetaStep(new CarbonMonoxide()), "detect CO()"); } @Test public void blockSteps() throws Exception { st.assertRoundTrip(new ExecutorStep(null), "node {\n // some block\n}"); st.assertRoundTrip(new ExecutorStep("linux"), "node('linux') {\n // some block\n}"); st.assertRoundTrip(new WorkspaceStep(null), "ws {\n // some block\n}"); st.assertRoundTrip(new WorkspaceStep("loc"), "ws('loc') {\n // some block\n}"); } @Issue("JENKINS-29922") @Test public void blockMetaSteps() throws Exception { st.assertRoundTrip(new CurveMetaStep(new Circle()), "circle {\n // some block\n}"); st.assertRoundTrip(new CurveMetaStep(new Polygon(5)), "polygon(5) {\n // some block\n}"); } @Test public void escapes() throws Exception { st.assertRoundTrip(new EchoStep("Bob's message \\/ here"), "echo 'Bob\\'s message \\\\/ here'"); } @Test public void multilineStrings() throws Exception { st.assertRoundTrip(new EchoStep("echo hello\necho 1/2 way\necho goodbye"), "echo '''echo hello\necho 1/2 way\necho goodbye'''"); } @Test public void buildTriggerStep() throws Exception { BuildTriggerStep step = new BuildTriggerStep("downstream"); st.assertRoundTrip(step, "build 'downstream'"); step.setParameters(Arrays.asList(new StringParameterValue("branch", "default"), new BooleanParameterValue("correct", true))); if (StringParameterDefinition.DescriptorImpl.class.isAnnotationPresent(Symbol.class)) { st.assertRoundTrip(step, "build job: 'downstream', parameters: [string(name: 'branch', value: 'default'), booleanParam(name: 'correct', value: true)]"); } else { // TODO 2.x delete st.assertRoundTrip(step, "build job: 'downstream', parameters: [[$class: 'StringParameterValue', name: 'branch', value: 'default'], [$class: 'BooleanParameterValue', name: 'correct', value: true]]"); } } @Issue("JENKINS-25779") @Test public void defaultValues() throws Exception { st.assertRoundTrip(new InputStep("Ready?"), "input 'Ready?'"); } @Issue("JENKINS-29922") @Test public void getQuasiDescriptors() throws Exception { String quasiDescriptors = new Snippetizer().getQuasiDescriptors(false).toString(); assertThat(quasiDescriptors, containsString("circle=Circle")); assertThat(quasiDescriptors, containsString("polygon=Polygon")); assertThat(quasiDescriptors, containsString("CO=CarbonMonoxide")); assertThat("State.moderate currently disqualifies this metastep", quasiDescriptors, not(containsString("california=California"))); } @Test public void generateSnippet() throws Exception { st.assertGenerateSnippet("{'stapler-class':'" + EchoStep.class.getName() + "', 'message':'hello world'}", "echo 'hello world'", null); st.assertGenerateSnippet("{'stapler-class':'" + Circle.class.getName() + "'}", "circle {\n // some block\n}", null); st.assertGenerateSnippet("{'stapler-class':'" + Polygon.class.getName() + "', 'n':5}", "polygon(5) {\n // some block\n}", null); st.assertGenerateSnippet("{'stapler-class':'" + CarbonMonoxide.class.getName() + "'}", "detect CO()", null); } @Issue("JENKINS-26093") @Test public void generateSnippetForBuildTrigger() throws Exception { MockFolder d1 = r.createFolder("d1"); FreeStyleProject ds = d1.createProject(FreeStyleProject.class, "ds"); MockFolder d2 = r.createFolder("d2"); // Really this would be a WorkflowJob, but we cannot depend on that here, and it should not matter since we are just looking for Job: FreeStyleProject us = d2.createProject(FreeStyleProject.class, "us"); ds.addProperty(new ParametersDefinitionProperty(new StringParameterDefinition("key", ""), new BooleanParameterDefinition("flag", false, ""))); String snippet; if (StringParameterDefinition.DescriptorImpl.class.isAnnotationPresent(Symbol.class)) { snippet = "build job: '../d1/ds', parameters: [string(name: 'key', value: 'stuff'), booleanParam(name: 'flag', value: true)]"; } else { // TODO 2.x delete snippet = "build job: '../d1/ds', parameters: [[$class: 'StringParameterValue', name: 'key', value: 'stuff'], [$class: 'BooleanParameterValue', name: 'flag', value: true]]"; } st.assertGenerateSnippet("{'stapler-class':'" + BuildTriggerStep.class.getName() + "', 'job':'../d1/ds', 'parameter': [{'name':'key', 'value':'stuff'}, {'name':'flag', 'value':true}]}", snippet, us.getAbsoluteUrl() + "configure"); } @Issue("JENKINS-29739") @Test public void generateSnippetForBuildTriggerSingle() throws Exception { FreeStyleProject ds = r.jenkins.createProject(FreeStyleProject.class, "ds1"); FreeStyleProject us = r.jenkins.createProject(FreeStyleProject.class, "us1"); ds.addProperty(new ParametersDefinitionProperty(new StringParameterDefinition("key", ""))); String snippet; if (StringParameterDefinition.DescriptorImpl.class.isAnnotationPresent(Symbol.class)) { snippet = "build job: 'ds1', parameters: [string(name: 'key', value: 'stuff')]"; } else { // TODO 2.x delete snippet = "build job: 'ds1', parameters: [[$class: 'StringParameterValue', name: 'key', value: 'stuff']]"; } st.assertGenerateSnippet("{'stapler-class':'" + BuildTriggerStep.class.getName() + "', 'job':'ds1', 'parameter': {'name':'key', 'value':'stuff'}}", snippet, us.getAbsoluteUrl() + "configure"); } @Test public void generateSnippetForBuildTriggerNone() throws Exception { FreeStyleProject ds = r.jenkins.createProject(FreeStyleProject.class, "ds0"); FreeStyleProject us = r.jenkins.createProject(FreeStyleProject.class, "us0"); st.assertGenerateSnippet("{'stapler-class':'" + BuildTriggerStep.class.getName() + "', 'job':'ds0'}", "build 'ds0'", us.getAbsoluteUrl() + "configure"); } @Test public void generateSnippetAdvancedDeprecated() throws Exception { st.assertGenerateSnippet("{'stapler-class':'" + CatchErrorStep.class.getName() + "'}", "// " + Messages.Snippetizer_this_step_should_not_normally_be_used_in() + "\ncatchError {\n // some block\n}", null); } @Issue("JENKINS-26126") @Test public void doDslRef() throws Exception { JenkinsRule.WebClient wc = r.createWebClient(); String html = wc.goTo(Snippetizer.ACTION_URL + "/html").getWebResponse().getContentAsString(); assertThat("text from LoadStep/help-path.html is included", html, containsString("the Groovy file to load")); assertThat("SubversionSCM.workspaceUpdater is mentioned as an attribute of a value of GenericSCMStep.delegate", html, containsString("workspaceUpdater")); assertThat("CheckoutUpdater is mentioned as an option", html, containsString("CheckoutUpdater")); assertThat("content is written to the end", html, containsString("</body></html>")); } @Issue({"JENKINS-35395", "JENKINS-38114"}) @Test public void doGlobalsRef() throws Exception { JenkinsRule.WebClient wc = r.createWebClient(); String html = wc.goTo(Snippetizer.ACTION_URL + "/globals").getWebResponse().getContentAsString(); assertThat("text from RunWrapperBinder/help.jelly is included", html, containsString("may be used to refer to the currently running build")); assertThat("text from RunWrapperBinder/help.jelly includes text from RunWrapper/help.html", html, containsString("<dt><code>buildVariables</code></dt>")); assertThat("content is written to the end", html, containsString("</body></html>")); } @Issue("JENKINS-26126") @Test public void doGdsl() throws Exception { JenkinsRule.WebClient wc = r.createWebClient(); String gdsl = wc.goTo(Snippetizer.ACTION_URL + "/gdsl", "text/plain").getWebResponse().getContentAsString(); assertThat("Description is included as doc", gdsl, containsString("Build a job")); assertThat("Timeout step appears", gdsl, containsString("name: 'timeout'")); // Verify valid groovy syntax. GroovyShell shell = new GroovyShell(r.jenkins.getPluginManager().uberClassLoader); shell.parse(gdsl); } @Issue("JENKINS-26126") @Test public void doDsld() throws Exception { JenkinsRule.WebClient wc = r.createWebClient(); String dsld = wc.goTo(Snippetizer.ACTION_URL + "/dsld", "text/plain").getWebResponse().getContentAsString(); assertThat("Description is included as doc", dsld, containsString("Build a job")); assertThat("Timeout step appears", dsld, containsString("name: 'timeout'")); // Verify valid groovy sntax. GroovyShell shell = new GroovyShell(r.jenkins.getPluginManager().uberClassLoader); shell.parse(dsld); } @Issue("JENKINS-29711") @Test public void monomorphic() throws Exception { MonomorphicStep monomorphicStep = new MonomorphicStep(new MonomorphicData("one", "two")); st.assertRoundTrip(monomorphicStep, "monomorphStep([firstArg: 'one', secondArg: 'two'])"); } @Issue("JENKINS-29711") @Test public void monomorphicList() throws Exception { List<MonomorphicData> dataList = new ArrayList<>(); dataList.add(new MonomorphicData("one", "two")); dataList.add(new MonomorphicData("three", "four")); MonomorphicListStep monomorphicStep = new MonomorphicListStep(dataList); st.assertRoundTrip(monomorphicStep, "monomorphListStep([[firstArg: 'one', secondArg: 'two'], [firstArg: 'three', secondArg: 'four']])"); } @Issue("JENKINS-29711") @Test public void monomorphicSymbol() throws Exception { MonomorphicWithSymbolStep monomorphicStep = new MonomorphicWithSymbolStep(new MonomorphicDataWithSymbol("one", "two")); st.assertRoundTrip(monomorphicStep, "monomorphWithSymbolStep monomorphSymbol(firstArg: 'one', secondArg: 'two')"); } @Issue("JENKINS-29711") @Test public void monomorphicListSymbol() throws Exception { List<MonomorphicDataWithSymbol> dataList = new ArrayList<>(); dataList.add(new MonomorphicDataWithSymbol("one", "two")); dataList.add(new MonomorphicDataWithSymbol("three", "four")); MonomorphicListWithSymbolStep monomorphicStep = new MonomorphicListWithSymbolStep(dataList); st.assertRoundTrip(monomorphicStep, "monomorphListSymbolStep([monomorphSymbol(firstArg: 'one', secondArg: 'two'), monomorphSymbol(firstArg: 'three', secondArg: 'four')])"); } @Test public void noArgStepDocs() throws Exception { SnippetizerTester.assertDocGeneration(PwdStep.class); } @Test public void singleArgStepDocs() throws Exception { SnippetizerTester.assertDocGeneration(EchoStep.class); } @Test public void oneOrMoreArgsStepDocs() throws Exception { SnippetizerTester.assertDocGeneration(InputStep.class); } @Test public void buildStepDocs() throws Exception { SnippetizerTester.assertDocGeneration(BuildTriggerStep.class); } @Test public void coreStepDocs() throws Exception { SnippetizerTester.assertDocGeneration(CoreStep.class); } /** * An example of a step that will fail to generate docs correctly due to a lack of a {@link org.kohsuke.stapler.DataBoundConstructor}. */ @Test(expected = NoStaplerConstructorException.class) public void parallelStepDocs() throws Exception { SnippetizerTester.assertDocGeneration(ParallelStep.class); } }