Monday, December 3, 2018

Jenkins 6: Advanced Configuration Area

Our build is advancing. Today we want to move optional fields into an advanced section and provide reasonable defaults for these entries.

Jenkins Tutorials

For a list of all jenkins related tutorials see Jenkins Tutorials Overview.

Source code for this tutorial is available on github as a single zip archive, as a Team Project Set or you can browse the files online.

Step 1: Advanced UI Section

To simplify the job setup we now move all parameters except the build message to an advanced section.
The only thing necessary in the config.jelly file is to create the section and move all affected input elements into it:
 <f:entry title="Custom build message" field="buildMessage">
  <f:textbox default="${descriptor.getDefaultBuildMessage()}" />
 </f:entry>

 <f:advanced>
  <f:entry title="Fail this build" field="failBuild">
   <f:checkbox />
  </f:entry>


  <f:entry title="Build Delay" field="buildDelay">
   <f:select />
  </f:entry>
 </f:advanced>

Afterwards the UI looks like this:


Step 2: Java Refactoring

Basically we do not need to change anything in the Java code to make this work. However we want to prepare a little for pipeline builds, so we remove non-required parameters from the constructor and create separate setters for them. To make Jenkins aware of these setters, use the @DataBoundSetter annotation:
public class HelloBuilder extends Builder implements SimpleBuildStep {

 private boolean fFailBuild = false;

 private String fBuildMessage;

 private String fBuildDelay = "none";

 @DataBoundConstructor
 public HelloBuilder(String buildMessage) {
  fBuildMessage = buildMessage;
 }

 @DataBoundSetter
 public void setFailBuild(boolean failBuild) {
  fFailBuild = failBuild;
 }
 
 @DataBoundSetter
 public void setBuildDelay(String buildDelay) {
  fBuildDelay = buildDelay;
 }
}

Whenever a parameter is not required, remove it from the constructor and use a setter for it.

Jenkins 5: Combo Boxes

Combo boxes are the next UI element we will add to our builder.

Jenkins Tutorials

For a list of all jenkins related tutorials see Jenkins Tutorials Overview.

Source code for this tutorial is available on github as a single zip archive, as a Team Project Set or you can browse the files online.

Step 1: UI Definition

In the config.jelly file we simply define that we want to use a combo box:
 <f:entry title="Build Delay" field="buildDelay">
  <f:select />
 </f:entry>
The definition does not contain entries to select. These will be populated by the Descriptor class.

Step 2: Item Definition

Jenkins will look for a method called doFill<field>Items in our Descriptor class to populate the combo. We are doing a first approach now to understand the scheme:
  public ListBoxModel doFillBuildDelayItems() {
   ListBoxModel model = new ListBoxModel();
   
   model.add(new Option("None", "none"));
   model.add(new Option("Short", "short"));
   model.add(new Option("Long", "long"));
   
   return model;
  }
ListBoxModel is basically an ArrayList of Option instances. The first string represents the text visible to the user, the second one the value that will actually be stored in our variable (see next step).

If we would populate the combo this way, the first item would always be selected by default, even if we re-open a job that was configured differently. The Option constructor allows for a third parameter defining the selected state. We then just need to know the value that got stored with the job definition. Therefore we can inject the desired query parameter into our method parameters:
  public ListBoxModel doFillBuildDelayItems(@QueryParameter String buildDelay) {
   ListBoxModel model = new ListBoxModel();

   model.add(new Option("None", "none", "none".equals(buildDelay)));
   model.add(new Option("Short", "short", "short".equals(buildDelay)));
   model.add(new Option("Long", "long" , "long".equals(buildDelay)));

   return model;
  }
Now buildDelay contains the value that got stored by the user when the build step was originally configured. By comparing its string representation we can set the right option in the combo. Typically combo options could be populated from an Enum. To reduce the risk of typos we could write a small helper to create our Options:
 public static Option createOption(Enum<?> enumOption, String jobOption) {
  return new Option(enumOption.toString(), enumOption.name(), enumOption.name().equals(jobOption));
 }

Step 3: Glueing it all together

Finally we need to extend our constructor with the new parameter. Then we can use it in our build step:
public class HelloBuilder extends Builder implements SimpleBuildStep {

 private String fBuildDelay;

 @DataBoundConstructor
 public HelloBuilder(boolean failBuild, String buildMessage, String buildDelay) {
  fBuildDelay = buildDelay;
 }

 @Override
 public void perform(Run<?, ?> run, FilePath workspace, Launcher launcher, TaskListener listener)
   throws InterruptedException, IOException {
  listener.getLogger().println("This is the Hello plugin!");
  listener.getLogger().println(getBuildMessage());

  switch (getBuildDelay()) {
  case "long":
   Thread.sleep(10 * 1000);
   break;

  case "short":
   Thread.sleep(3 * 1000);
   break;

  case "none":
   // fall through
  default:
   // nothing to do
  }

  if (isFailBuild())
   throw new AbortException("Build error forced by plugin settings");
 }

 public String getBuildDelay() {
  return fBuildDelay;
 }
}

Jenkins 4: Unit Tests

Now that our builder plugin is working we should start writing some unit tests for it.

Jenkins Tutorials

For a list of all jenkins related tutorials see Jenkins Tutorials Overview.

Source code for this tutorial is available on github as a single zip archive, as a Team Project Set or you can browse the files online.

Step 1: Wrinting a simple test case

Jenkins tests can be written as JUnit tests. The test instance needed for execution tests can be created using a JUnit Rule.

Create a new JUnit Test Case com.codeandme.jenkins.builder.HelloBuilderTest in the src/test/java folder:
public class HelloBuilderTest {

 @Rule
 public JenkinsRule fJenkinsInstance = new JenkinsRule();
 
 @Test
 public void successfulBuild() throws Exception {
  HelloBuilder builder = new HelloBuilder(false, "JUnit test run");
  
  FreeStyleProject job = fJenkinsInstance.createFreeStyleProject();
  job.getBuildersList().add(builder);
  FreeStyleBuild build = fJenkinsInstance.buildAndAssertSuccess(job);
  
  fJenkinsInstance.assertLogContains("JUnit test run", build);
 }
}
In line 4 we create a test instance for our unit test. This instance is used from line 10 onwards to create and run our test job. The instance provides a set of assertion commands which we use to check the build result and the log output of the job execution.

You can run these tests as JUnit tests right from Eclipse or you can execute them via maven by running
mvn test

Step 2: A test expecting an execution fail

We use the same approach as before. To check for a failed build we need to run the build job a little bit different:
 @Test
 public void failedBuild() throws Exception {
  HelloBuilder builder = new HelloBuilder(true, "JUnit test fail");
  
  FreeStyleProject job = fJenkinsInstance.createFreeStyleProject();
  job.getBuildersList().add(builder);
  QueueTaskFuture<FreeStyleBuild> buildResult = job.scheduleBuild2(0);
  
  fJenkinsInstance.assertBuildStatus(Result.FAILURE, buildResult);
  fJenkinsInstance.assertLogContains("JUnit test fail", buildResult.get());
 }