Skip to content

Commit 6506e01

Browse files
committed
Fixed an issue with pipeline configuration setup, added documentation.
1 parent e2c2317 commit 6506e01

File tree

9 files changed

+206
-27
lines changed

9 files changed

+206
-27
lines changed

README.md

Lines changed: 115 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,38 +5,47 @@
55
[![Jenkins Plugin](https://img.shields.io/jenkins/plugin/v/multiselect-parameter.svg)](https://plugins.jenkins.io/multiselect-parameter)
66
[![GitHub release](https://img.shields.io/github/release/jenkinsci/multiselect-parameter-plugin.svg?label=changelog)](https://github.com/jenkinsci/multiselect-parameter-plugin/releases/latest)
77
[![Jenkins Plugin Installs](https://img.shields.io/jenkins/plugin/i/multiselect-parameter.svg?color=blue)](https://plugins.jenkins.io/multiselect-parameter)
8+
[![codecov](https://codecov.io/github/swesteme/multiselect-parameter-plugin/branch/main/graph/badge.svg?token=SFXR05LMD4)](https://codecov.io/github/swesteme/multiselect-parameter-plugin)
89

910
## Introduction
1011

11-
The Multiselect parameter plugin is an extension to Jenkins parameterized builds. It allows Jenkins users to
12-
create more complex variables consisting of a configurable number of interdependent drop down boxes.
12+
The Multiselect parameter plugin is an extension to Jenkins parameterized builds. It allows Jenkins users to
13+
create more complex variables consisting of a configurable number of interdependent drop-down boxes.
1314

1415
![Example select boxes](images/sample_build.png)
1516

1617
## Getting started
1718

18-
As soon as the plugin has been installed in Jenkins, freestyle jobs offer a new type of build parameters,
19-
named "Multiselect parameter". A simple form allows the configuration of a parameter name, a description
19+
As soon as the plugin has been installed in Jenkins, parameterized jobs offer a new type of build parameters,
20+
named "Multiselect parameter".
21+
22+
There are two configuration variants described below: one for *standard* Jenkins jobs, one for the popular *pipelines*,
23+
which allow for programmatic job configuration.
24+
25+
### "Standard" job configuration
26+
27+
A simple configuration form allows the setup of a parameter name, a description
2028
and a CSV configuration to use in creation of dependency tree.
2129

2230
There are four different types of rows used to describe the parameter and its values/variables:
2331

24-
* H: header labels for the drop down boxes, one per variable
32+
* H: header labels for the drop-down boxes, one per variable
2533
* V: variable names used as environment variables in build process
2634
* T: titles for the content items in the following row (optional, fallback is the content itself as a label)
2735
* C: the content items that will be passed in the environment variables upon selection
2836

2937
The rows "H" and "V" only appear once as first and second row. The rows "T" and "C" are repeated.
3038

31-
Note: the "T" rows are optional, but may be useful for more technical content values that need a little bit of extra explanation.
39+
Note: the "T" rows are optional, but may be useful for more technical content values that need a little bit of extra
40+
explanation.
3241

3342
![Example configuration](images/sample_configuration.png)
3443

35-
## Example
44+
#### Example
3645

3746
In the following simple example, the "build with parameters" step requests to select a favourite team in
3847
a number of sports categories and countries.
39-
48+
4049
```csv
4150
H,Sport,Team
4251
V,SELECTED_SPORT,SELECTED_TEAM
@@ -48,7 +57,101 @@ C,Football,FC Rumeln
4857
C,Wakeboard,WSC Duisburg Rheinhausen
4958
```
5059

51-
This will display two drop down boxes. One with the label "Sport", one with the label "Team".
52-
The first drop down box contains the values "Tennis", "Football" and "Wakeboard", the second will only hold the values "Tennisclub Rumeln-Kaldenhausen e. V." and "Oppumer TC".
53-
When the first value is switched to "Wakeboard", the second drop down boxes content will change to "WSC Duisburg Rheinhausen".
54-
As soon as the build is started, the environment variables SELECTED_SPORT and SELECTED_TEAM will contain the selected values.
60+
This will display two drop-down boxes. One with the label "Sport", one with the label "Team".
61+
The first drop-down box contains the values "Tennis", "Football" and "Wakeboard", the second will only hold the values "
62+
Tennisclub Rumeln-Kaldenhausen e. V." and "Oppumer TC".
63+
When the first value is switched to "Wakeboard", the second drop-down boxes content will change to "WSC Duisburg
64+
Rheinhausen".
65+
As soon as the build is started, the environment variables SELECTED_SPORT and SELECTED_TEAM will contain the selected
66+
values.
67+
68+
### Pipeline job configuration
69+
70+
The same example from above would be configured in a tree-structure, which can be created programmatically and resembles
71+
the internal structure that would be created from CSV internally.
72+
73+
![Pipeline configuration](images/sample_pipeline_config.png)
74+
75+
The light blue box shows the definition and configuration of the first drop-down box. The purple box shows the
76+
definition of values for the second drop-down box. Note that this is a simple example with only two combo boxes. It is
77+
possible to define any number of variable descriptions here, in which case the items in the purple boxes would also
78+
get "children".
79+
80+
#### Example
81+
82+
The following source code shows the example as complete (very simple) pipeline script.
83+
84+
```groovy
85+
pipeline {
86+
agent any
87+
stages {
88+
stage('Parameters') {
89+
steps {
90+
script {
91+
properties([
92+
parameters([
93+
multiselect(
94+
decisionTree: [$class : 'de.westemeyer.plugins.multiselect.MultiselectDecisionTree',
95+
variableDescriptions: ([
96+
[$class : 'de.westemeyer.plugins.multiselect.MultiselectVariableDescriptor',
97+
label : "Sport",
98+
variableName: "SELECTED_SPORT"
99+
],
100+
[$class : 'de.westemeyer.plugins.multiselect.MultiselectVariableDescriptor',
101+
label : "Team",
102+
variableName: "SELECTED_TEAM"
103+
]
104+
]),
105+
itemList : ([
106+
[$class : 'de.westemeyer.plugins.multiselect.MultiselectDecisionItem',
107+
value : "Tennis",
108+
children: ([
109+
[$class: 'de.westemeyer.plugins.multiselect.MultiselectDecisionItem',
110+
value : "Tennisclub Rumeln-Kaldenhausen e. V."
111+
],
112+
[$class: 'de.westemeyer.plugins.multiselect.MultiselectDecisionItem',
113+
label : "Alternative label",
114+
value : "Oppumer TC"
115+
]
116+
])
117+
],
118+
[$class : 'de.westemeyer.plugins.multiselect.MultiselectDecisionItem',
119+
value : "Football",
120+
children: ([
121+
[$class: 'de.westemeyer.plugins.multiselect.MultiselectDecisionItem',
122+
value : "Rumelner TV"
123+
],
124+
[$class: 'de.westemeyer.plugins.multiselect.MultiselectDecisionItem',
125+
value : "FC Rumeln"
126+
]
127+
])
128+
],
129+
[$class : 'de.westemeyer.plugins.multiselect.MultiselectDecisionItem',
130+
value : "Wakeboard",
131+
children: ([
132+
[$class: 'de.westemeyer.plugins.multiselect.MultiselectDecisionItem',
133+
value : "WSC Duisburg Rheinhausen"
134+
]
135+
])
136+
]
137+
]),
138+
],
139+
description: 'Please select your favourite team!',
140+
name: 'Favourite team'
141+
)
142+
])
143+
])
144+
}
145+
146+
}
147+
}
148+
149+
stage('Print variables') {
150+
steps {
151+
sh 'set'
152+
}
153+
}
154+
}
155+
}
156+
157+
```

images/sample_pipeline_config.png

397 KB
Loading

src/main/java/de/westemeyer/plugins/multiselect/MultiselectDecisionTree.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import de.westemeyer.plugins.multiselect.parser.ConfigSerialization;
44
import edu.umd.cs.findbugs.annotations.NonNull;
5+
import org.kohsuke.stapler.DataBoundConstructor;
56
import org.kohsuke.stapler.DataBoundSetter;
67

78
import java.io.ByteArrayInputStream;
@@ -42,6 +43,12 @@ public class MultiselectDecisionTree implements Serializable {
4243
@NonNull
4344
private List<MultiselectVariableDescriptor> variableDescriptions = new ArrayList<>();
4445

46+
@DataBoundConstructor
47+
public MultiselectDecisionTree() {
48+
// empty constructor is necessary, otherwise pipelines can not create
49+
// structured configuration (using setter methods)
50+
}
51+
4552
/**
4653
* Get initial values for column when first displaying list of select boxes in "build with parameters" view.
4754
* @param column column number

src/main/java/de/westemeyer/plugins/multiselect/MultiselectVariableDescriptor.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,9 @@ public class MultiselectVariableDescriptor implements Serializable {
3030

3131
/**
3232
* Create new variable description object.
33-
* @param label variable label
33+
* @param label variable label
3434
* @param variableName variable name
35-
* @param columnIndex index of this column
35+
* @param columnIndex index of this column
3636
*/
3737
@DataBoundConstructor
3838
public MultiselectVariableDescriptor(String label, String variableName, int columnIndex) {

src/main/java/de/westemeyer/plugins/multiselect/parser/CsvParser.java

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ public MultiselectDecisionTree analyzeConfiguration(InputStream config) {
4242
InputStreamReader reader = new InputStreamReader(config, StandardCharsets.UTF_8);
4343

4444
// create a new csv reader object
45-
try (CSVReader csvReader = new CSVReaderBuilder(reader).build()) {
45+
try (CSVReader csvReader = createCsvReader(reader)) {
4646

4747
// references for different kinds of rows
4848
List<String> headers = null;
@@ -137,11 +137,15 @@ public MultiselectDecisionTree analyzeConfiguration(InputStream config) {
137137
return decisionTree;
138138
}
139139

140+
protected CSVReader createCsvReader(InputStreamReader reader) {
141+
return new CSVReaderBuilder(reader).build();
142+
}
143+
140144
/**
141145
* Validate the lists of variable names and values. List of values may not be longer than the list of variable names.
142146
* @param variableNames list of variable names
143-
* @param index current column number
144-
* @param subList list of values
147+
* @param index current column number
148+
* @param subList list of values
145149
*/
146150
private void ensureMatchingListLengths(List<String> variableNames, int index, List<String> subList) {
147151
if (variableNames != null && subList.size() > variableNames.size()) {

src/main/java/de/westemeyer/plugins/multiselect/parser/CvwWriterVisitor.java

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -82,10 +82,7 @@ private void reverseAndWrite(String type, List<String> values) {
8282
* @return whether appended value was not null or empty
8383
*/
8484
boolean appendValue(List<String> columns, String value) {
85-
if (value != null) {
86-
columns.add(value);
87-
return !value.isEmpty();
88-
}
89-
return false;
85+
columns.add(value);
86+
return !(value == null || value.isEmpty());
9087
}
9188
}

src/test/java/de/westemeyer/plugins/multiselect/MultiselectDecisionTreeTest.java

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
package de.westemeyer.plugins.multiselect;
22

33
import de.westemeyer.plugins.multiselect.parser.ConfigSerialization;
4+
import de.westemeyer.plugins.multiselect.parser.CsvWriter;
45
import edu.umd.cs.findbugs.annotations.NonNull;
56
import org.junit.jupiter.api.Assertions;
67
import org.junit.jupiter.api.Test;
78
import org.junit.jupiter.params.ParameterizedTest;
89
import org.junit.jupiter.params.provider.CsvSource;
910

11+
import java.io.ByteArrayOutputStream;
1012
import java.io.OutputStream;
1113
import java.util.Arrays;
1214
import java.util.Collections;
@@ -96,4 +98,47 @@ public List<MultiselectDecisionItem> getItemList() {
9698

9799
Assertions.assertEquals(0, decisionTree.getInitialValuesForColumn(0).size());
98100
}
101+
102+
@Test
103+
void serializationRoundTripTest() throws Exception {
104+
MultiselectDecisionTree decisionTree = new MultiselectDecisionTree();
105+
decisionTree.setVariableDescriptions(Arrays.asList(createDescriptor("Sport", "SELECTED_SPORT"), createDescriptor("Team", "SELECTED_TEAM")));
106+
decisionTree.setItemList(Arrays.asList(
107+
createItem(null, "Tennis", createItem(null, "Tennisclub Rumeln-Kaldenhausen e.V."), createItem("Alternative label", "Oppumer TC")),
108+
createItem(null, "Football", createItem(null, "Rumelner TV"), createItem(null, "FC Rumeln")),
109+
createItem("Very popular sport", "Wakeboard", createItem(null, "WSC Duisburg Rheinhausen"))));
110+
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
111+
decisionTree.serialize(new CsvWriter(), outputStream);
112+
Assertions.assertEquals("H,Sport,Team\n" +
113+
"V,SELECTED_SPORT,SELECTED_TEAM\n" +
114+
"C,Tennis,Tennisclub Rumeln-Kaldenhausen e.V.\n" +
115+
"T,,Alternative label\n" +
116+
"C,Tennis,Oppumer TC\n" +
117+
"C,Football,Rumelner TV\n" +
118+
"C,Football,FC Rumeln\n" +
119+
"T,Very popular sport,\n" +
120+
"C,Wakeboard,WSC Duisburg Rheinhausen\n", outputStream.toString());
121+
}
122+
}
123+
124+
private MultiselectDecisionItem createItem(String label, String value, MultiselectDecisionItem... children) {
125+
MultiselectDecisionItem item = new MultiselectDecisionItem(null, null, null);
126+
if (label != null) {
127+
item.setLabel(label);
128+
}
129+
if (value != null) {
130+
item.setValue(value);
131+
}
132+
if (children != null) {
133+
item.setChildren(Arrays.asList(children));
134+
}
135+
return item;
136+
}
137+
138+
private MultiselectVariableDescriptor createDescriptor(String label, String variable) {
139+
MultiselectVariableDescriptor descriptor = new MultiselectVariableDescriptor(null, null, 0);
140+
descriptor.setLabel(label);
141+
descriptor.setVariableName(variable);
142+
return descriptor;
143+
}
99144
}

src/test/java/de/westemeyer/plugins/multiselect/parser/CsvParserTest.java

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
package de.westemeyer.plugins.multiselect.parser;
22

3+
import com.opencsv.CSVReader;
34
import de.westemeyer.plugins.multiselect.MultiselectDecisionTree;
45
import org.junit.jupiter.api.Assertions;
56
import org.junit.jupiter.api.Test;
67
import org.junit.jupiter.params.ParameterizedTest;
78
import org.junit.jupiter.params.provider.ValueSource;
89

10+
import java.io.BufferedReader;
11+
import java.io.ByteArrayInputStream;
912
import java.io.ByteArrayOutputStream;
13+
import java.io.IOException;
14+
import java.io.InputStreamReader;
1015

1116
class CsvParserTest {
1217
/** First input csv for tests. */
@@ -17,10 +22,10 @@ class CsvParserTest {
1722

1823
/** Input from issue JENKINS-66486. */
1924
private static final String INPUT_QUOTED = "H,Component,Container,Machine\n"
20-
+ "V,SELECTED_COMPONENT,SELECTED_CONTAINER,MACHINES\n"
21-
+ "C,component1,container1,\"machine1,machine2\"\n"
22-
+ "C,component2,container1,\"machine3,machine4\"\n"
23-
+ "C,component3,container2,\"machine1,machine2\"\n";
25+
+ "V,SELECTED_COMPONENT,SELECTED_CONTAINER,MACHINES\n"
26+
+ "C,component1,container1,\"machine1,machine2\"\n"
27+
+ "C,component2,container1,\"machine3,machine4\"\n"
28+
+ "C,component3,container2,\"machine1,machine2\"\n";
2429

2530
@ParameterizedTest
2631
@ValueSource(strings = {INPUT_CSV, INPUT_NO_TITLES, "", "V,A,B\n", "H,Hello,World\n", "C,a,b\n", INPUT_QUOTED})
@@ -46,6 +51,24 @@ void invalidColumnCount() throws Exception {
4651
Assertions.assertTrue(decisionTree.getVariableLabels().isEmpty());
4752
}
4853

54+
@Test
55+
void testException() throws IOException {
56+
try (ByteArrayInputStream inputStream = new ByteArrayInputStream("".getBytes())) {
57+
CsvParser csvParser = new CsvParser() {
58+
@Override
59+
protected CSVReader createCsvReader(InputStreamReader reader) {
60+
return new CSVReader(new BufferedReader(new InputStreamReader(inputStream))) {
61+
@Override
62+
public void close() throws IOException {
63+
throw new IOException("Ooops, I might get caught!");
64+
}
65+
};
66+
}
67+
};
68+
Assertions.assertDoesNotThrow(() -> csvParser.analyzeConfiguration(inputStream));
69+
}
70+
}
71+
4972
private MultiselectDecisionTree getDecisionTree(String input, boolean assertEquality) throws Exception {
5073
// parse input stream to tree meta object
5174
MultiselectDecisionTree decisionTree = MultiselectDecisionTree.parse(input);

src/test/java/de/westemeyer/plugins/multiselect/parser/CvwWriterVisitorTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,6 @@ void appendValue() {
1919
assertTrue(cvwWriterVisitor.appendValue(result, "Hello"));
2020
assertEquals(2, result.size());
2121
assertFalse(cvwWriterVisitor.appendValue(result, null));
22-
assertEquals(2, result.size());
22+
assertEquals(3, result.size());
2323
}
2424
}

0 commit comments

Comments
 (0)