diff --git a/src/main/java/hudson/tasks/junit/ClassResult.java b/src/main/java/hudson/tasks/junit/ClassResult.java
index 65a8931e6..728959b90 100644
--- a/src/main/java/hudson/tasks/junit/ClassResult.java
+++ b/src/main/java/hudson/tasks/junit/ClassResult.java
@@ -102,6 +102,11 @@ public String getChildTitle() {
return "Class Results";
}
+ @Override
+ public String getChildType() {
+ return "case";
+ }
+
@Exported(visibility=999)
@Override
public String getName() {
diff --git a/src/main/java/hudson/tasks/junit/PackageResult.java b/src/main/java/hudson/tasks/junit/PackageResult.java
index a0c71f7bf..8ca0d5c22 100644
--- a/src/main/java/hudson/tasks/junit/PackageResult.java
+++ b/src/main/java/hudson/tasks/junit/PackageResult.java
@@ -121,6 +121,11 @@ public String getChildTitle() {
return Messages.PackageResult_getChildTitle();
}
+ @Override
+ public String getChildType() {
+ return "class";
+ }
+
// TODO: wait until stapler 1.60 to do this @Exported
@Override
public float getDuration() {
diff --git a/src/main/java/hudson/tasks/junit/TestAction.java b/src/main/java/hudson/tasks/junit/TestAction.java
index e604c7d1c..f4db1b6a5 100644
--- a/src/main/java/hudson/tasks/junit/TestAction.java
+++ b/src/main/java/hudson/tasks/junit/TestAction.java
@@ -33,6 +33,10 @@
*
index.jelly: included at the top of the test page
* summary.jelly: included in a collapsed panel on the test parent page
* badge.jelly: shown after the test link on the test parent page
+ * casetableheader.jelly: allows additional table headers to be shown in tables that list test methods
+ * classtableheader.jelly: allows additional table headers to be shown in tables that list test classes
+ * packagetableheader.jelly: allows additional table headers to be shown in tables that list test packages
+ * tablerow.jelly: allows additional table cells to be shown in tables that list test methods, classes and packages
*
*
* @author tom
diff --git a/src/main/java/hudson/tasks/junit/TestResult.java b/src/main/java/hudson/tasks/junit/TestResult.java
index 0eb11f5fc..900b84e04 100644
--- a/src/main/java/hudson/tasks/junit/TestResult.java
+++ b/src/main/java/hudson/tasks/junit/TestResult.java
@@ -479,6 +479,11 @@ public String getChildTitle() {
return Messages.TestResult_getChildTitle();
}
+ @Override
+ public String getChildType() {
+ return "package";
+ }
+
@Exported(visibility=999)
@Override
public float getDuration() {
diff --git a/src/main/java/hudson/tasks/test/TabulatedResult.java b/src/main/java/hudson/tasks/test/TabulatedResult.java
index e4fda502f..778234d8e 100644
--- a/src/main/java/hudson/tasks/test/TabulatedResult.java
+++ b/src/main/java/hudson/tasks/test/TabulatedResult.java
@@ -121,4 +121,14 @@ public TabulatedResult blockToTestResult(@NonNull PipelineBlockWithTests block,
public String getChildTitle() {
return "";
}
+
+ /**
+ * Get a simple name for the type of children the {@link #getChildren()} method returns, for example "case", "class"
+ * or "package".
+ *
+ * @return the type of children this result has, all lowercase.
+ */
+ public String getChildType() {
+ return "";
+ }
}
diff --git a/src/main/resources/hudson/tasks/junit/ClassResult/body.jelly b/src/main/resources/hudson/tasks/junit/ClassResult/body.jelly
index 6ce00003d..e3790230c 100644
--- a/src/main/resources/hudson/tasks/junit/ClassResult/body.jelly
+++ b/src/main/resources/hudson/tasks/junit/ClassResult/body.jelly
@@ -32,6 +32,9 @@ THE SOFTWARE.
+
+
+
@@ -50,6 +53,9 @@ THE SOFTWARE.
${pst.message}
+
+
+
diff --git a/src/main/resources/hudson/tasks/test/MetaTabulatedResult/body.jelly b/src/main/resources/hudson/tasks/test/MetaTabulatedResult/body.jelly
index 9bb30317d..597dfa54a 100644
--- a/src/main/resources/hudson/tasks/test/MetaTabulatedResult/body.jelly
+++ b/src/main/resources/hudson/tasks/test/MetaTabulatedResult/body.jelly
@@ -27,12 +27,15 @@ THE SOFTWARE.
${%All Failed Tests}
-
+
+
+
+
@@ -44,6 +47,9 @@ THE SOFTWARE.
${f.age}
|
+
+
+
@@ -64,6 +70,9 @@ THE SOFTWARE.
+
+
+
@@ -94,6 +103,9 @@ THE SOFTWARE.
${h.getDiffString2(p.totalCount-prev.totalCount)}
|
+
+
+
diff --git a/src/test/java/hudson/tasks/junit/CustomColumnsTest.java b/src/test/java/hudson/tasks/junit/CustomColumnsTest.java
new file mode 100644
index 000000000..ee9078da3
--- /dev/null
+++ b/src/test/java/hudson/tasks/junit/CustomColumnsTest.java
@@ -0,0 +1,113 @@
+package hudson.tasks.junit;
+
+import com.gargoylesoftware.htmlunit.html.HtmlPage;
+import com.gargoylesoftware.htmlunit.html.HtmlTable;
+import com.gargoylesoftware.htmlunit.html.HtmlTableCell;
+import hudson.Launcher;
+import hudson.model.AbstractBuild;
+import hudson.model.BuildListener;
+import hudson.model.FreeStyleBuild;
+import hudson.model.FreeStyleProject;
+import hudson.model.Result;
+import hudson.tasks.junit.rot13.Rot13Publisher;
+import hudson.tasks.test.helper.WebClientFactory;
+import org.apache.commons.lang3.tuple.Pair;
+import org.hamcrest.CoreMatchers;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.jvnet.hudson.test.JenkinsRule;
+import org.jvnet.hudson.test.TestBuilder;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Verifies that TestDataPublishers can contribute custom columns to html tables in result pages.
+ */
+public class CustomColumnsTest {
+
+ @Rule
+ public JenkinsRule jenkins = new JenkinsRule();
+
+ private FreeStyleProject project;
+
+ private static final String reportFileName = "junit-report-494.xml";
+
+ @Before
+ public void setUp() throws Exception {
+ project = jenkins.createFreeStyleProject("customcolumns");
+ JUnitResultArchiver archiver = new JUnitResultArchiver("*.xml");
+ archiver.setTestDataPublishers(Collections.singletonList(new Rot13Publisher()));
+ archiver.setSkipPublishingChecks(true);
+ project.getPublishersList().add(archiver);
+ project.getBuildersList().add(new TestBuilder() {
+ @Override
+ public boolean perform(AbstractBuild, ?> build, Launcher launcher, BuildListener listener)
+ throws InterruptedException, IOException {
+ build.getWorkspace().child(reportFileName).copyFrom(getClass().getResource(reportFileName));
+ return true;
+ }
+ });
+ FreeStyleBuild build = project.scheduleBuild2(0).get(5, TimeUnit.MINUTES);
+ jenkins.assertBuildStatus(Result.UNSTABLE, build);
+ }
+
+ @SafeVarargs
+ private void verifyThatTableContainsExpectedValues(String pathToPage, String tableId, String headerName,
+ Pair... rowValues) throws Exception {
+
+ JenkinsRule.WebClient wc = WebClientFactory.createWebClientWithDisabledJavaScript(jenkins);
+ HtmlPage projectPage = wc.getPage(project);
+ jenkins.assertGoodStatus(projectPage);
+ HtmlPage classReportPage = wc.getPage(project, pathToPage);
+ jenkins.assertGoodStatus(classReportPage);
+
+ HtmlTable testResultTable = (HtmlTable) classReportPage.getFirstByXPath("//table[@id='" + tableId + "']");
+ List headerRowCells = testResultTable.getHeader().getRows().get(0).getCells();
+ int numberOfColumns = headerRowCells.size();
+ assertEquals(headerName, headerRowCells.get(numberOfColumns - 1).asNormalizedText());
+
+ for (int x = 0; x < rowValues.length; x++) {
+ List bodyRowCells = testResultTable.getBodies().get(0).getRows().get(x).getCells();
+ assertThat(bodyRowCells.get(0).asNormalizedText(), CoreMatchers.containsString(rowValues[x].getLeft()));
+ assertEquals(rowValues[x].getRight(), bodyRowCells.get(numberOfColumns - 1).asNormalizedText());
+ }
+ }
+
+ @Test
+ public void verifyThatCustomColumnIsAddedToTheTestsTableOnTheClassResultPage() throws Exception {
+ verifyThatTableContainsExpectedValues("/lastBuild/testReport/junit/io.jenkins.example/AnExampleTestClass/",
+ "testresult", "ROT13 for cases on class page", Pair.of("testCaseA", "grfgPnfrN for case"), Pair.of(
+ "testCaseZ", "grfgPnfrM for case"));
+ }
+
+ @Test
+ public void verifyThatCustomColumnIsAddedToTheFailedTestsTableOnThePackageResultPage() throws Exception {
+ verifyThatTableContainsExpectedValues("/lastBuild/testReport/junit/io.jenkins.example/", "failedtestresult",
+ "ROT13 for failed cases on package page", Pair.of("testCaseA", "grfgPnfrN for case"));
+ }
+
+ @Test
+ public void verifyThatCustomColumnIsAddedToTheClassesTableOnThePackageResultPage() throws Exception {
+ verifyThatTableContainsExpectedValues("/lastBuild/testReport/junit/io.jenkins.example/", "testresult",
+ "ROT13 for all classes on package page", Pair.of("AnExampleTestClass", "NaRknzcyrGrfgPynff for class"));
+ }
+
+ @Test
+ public void verifyThatCustomColumnIsAddedToTheFailedTestsTableOnTheTestResultPage() throws Exception {
+ verifyThatTableContainsExpectedValues("/lastBuild/testReport/", "failedtestresult",
+ "ROT13 for failed cases on test page", Pair.of("testCaseA", "grfgPnfrN for case"));
+ }
+
+ @Test
+ public void verifyThatCustomColumnIsAddedToTheClassesTableOnTheTestResultPage() throws Exception {
+ verifyThatTableContainsExpectedValues("/lastBuild/testReport/", "testresult",
+ "ROT13 for all packages on test page", Pair.of("io.jenkins.example", "vb.wraxvaf.rknzcyr for package"));
+ }
+}
diff --git a/src/test/java/hudson/tasks/junit/rot13/Rot13CaseAction.java b/src/test/java/hudson/tasks/junit/rot13/Rot13CaseAction.java
new file mode 100644
index 000000000..a72d10671
--- /dev/null
+++ b/src/test/java/hudson/tasks/junit/rot13/Rot13CaseAction.java
@@ -0,0 +1,9 @@
+package hudson.tasks.junit.rot13;
+
+public class Rot13CaseAction extends Rot13CipherAction {
+
+ public Rot13CaseAction(String ciphertext) {
+ super(ciphertext);
+ }
+
+}
diff --git a/src/test/java/hudson/tasks/junit/rot13/Rot13CipherAction.java b/src/test/java/hudson/tasks/junit/rot13/Rot13CipherAction.java
new file mode 100644
index 000000000..6b75ed354
--- /dev/null
+++ b/src/test/java/hudson/tasks/junit/rot13/Rot13CipherAction.java
@@ -0,0 +1,32 @@
+package hudson.tasks.junit.rot13;
+
+import hudson.tasks.junit.TestAction;
+
+public abstract class Rot13CipherAction extends TestAction {
+
+ private final String ciphertext;
+
+ public Rot13CipherAction(String ciphertext) {
+ this.ciphertext = ciphertext;
+ }
+
+ @Override
+ public final String getIconFileName() {
+ return null;
+ }
+
+ @Override
+ public final String getDisplayName() {
+ return null;
+ }
+
+ @Override
+ public String getUrlName() {
+ return null;
+ }
+
+ public String getCiphertext() {
+ return ciphertext;
+ }
+
+}
diff --git a/src/test/java/hudson/tasks/junit/rot13/Rot13ClassAction.java b/src/test/java/hudson/tasks/junit/rot13/Rot13ClassAction.java
new file mode 100644
index 000000000..859a72b89
--- /dev/null
+++ b/src/test/java/hudson/tasks/junit/rot13/Rot13ClassAction.java
@@ -0,0 +1,9 @@
+package hudson.tasks.junit.rot13;
+
+public class Rot13ClassAction extends Rot13CipherAction {
+
+ public Rot13ClassAction(String ciphertext) {
+ super(ciphertext);
+ }
+
+}
diff --git a/src/test/java/hudson/tasks/junit/rot13/Rot13PackageAction.java b/src/test/java/hudson/tasks/junit/rot13/Rot13PackageAction.java
new file mode 100644
index 000000000..21fde55aa
--- /dev/null
+++ b/src/test/java/hudson/tasks/junit/rot13/Rot13PackageAction.java
@@ -0,0 +1,9 @@
+package hudson.tasks.junit.rot13;
+
+public class Rot13PackageAction extends Rot13CipherAction {
+
+ public Rot13PackageAction(String ciphertext) {
+ super(ciphertext);
+ }
+
+}
diff --git a/src/test/java/hudson/tasks/junit/rot13/Rot13Publisher.java b/src/test/java/hudson/tasks/junit/rot13/Rot13Publisher.java
new file mode 100644
index 000000000..4ecc41769
--- /dev/null
+++ b/src/test/java/hudson/tasks/junit/rot13/Rot13Publisher.java
@@ -0,0 +1,121 @@
+package hudson.tasks.junit.rot13;
+
+import hudson.Extension;
+import hudson.FilePath;
+import hudson.Launcher;
+import hudson.model.Descriptor;
+import hudson.model.Run;
+import hudson.model.TaskListener;
+import hudson.tasks.junit.CaseResult;
+import hudson.tasks.junit.ClassResult;
+import hudson.tasks.junit.PackageResult;
+import hudson.tasks.junit.TestAction;
+import hudson.tasks.junit.TestDataPublisher;
+import hudson.tasks.junit.TestResult;
+import hudson.tasks.junit.TestResultAction;
+import hudson.tasks.test.TestObject;
+import org.jenkinsci.Symbol;
+import org.kohsuke.stapler.DataBoundConstructor;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * A TestDataPublisher that adds a custom column containing the ROT13-encoded name of each test case, class and package
+ * in html tables across result pages. This publisher is intended to be used for validating that custom columns work as
+ * expected, but also as an illustration of how to provide custom columns. In this publisher, the values in the columns
+ * explicitly state where they're expected to be displayed (for example "ROT13 for failed cases on package page") to
+ * make it possible to validate that the correct jelly view is used in the correct page. Real-life implementations of
+ * custom columns will likely not need to distinguish between different pages and can get away with fewer TestActions,
+ * allowing greater reuse of jelly views.
+ */
+public class Rot13Publisher extends TestDataPublisher {
+
+ @DataBoundConstructor
+ public Rot13Publisher() {
+ }
+
+ @Override
+ public Data contributeTestData(Run, ?> build, FilePath workspace, Launcher launcher, TaskListener listener,
+ TestResult testResult) throws IOException, InterruptedException {
+ Map ciphertextMap = new HashMap<>();
+ for (PackageResult packageResult : testResult.getChildren()) {
+ ciphertextMap.put(packageResult.getName(), rot13(packageResult.getName()));
+ for (ClassResult classResult : packageResult.getChildren()) {
+ ciphertextMap.put(classResult.getFullName(), rot13(classResult.getName()));
+ for (CaseResult caseResult : classResult.getChildren()) {
+ ciphertextMap.put(caseResult.getFullName(), rot13(caseResult.getName()));
+ }
+ }
+ }
+ return new Data(ciphertextMap);
+ }
+
+ private static String rot13(String cleartext) {
+ StringBuilder ciphertext = new StringBuilder();
+ cleartext.chars().forEach(c -> {
+ if ('a' <= c && c <= 'z') {
+ c = c + 13;
+ if ('z' < c) {
+ c = c - 26;
+ }
+ }
+ if ('A' <= c && c <= 'Z') {
+ c = c + 13;
+ if ('Z' < c) {
+ c = c - 26;
+ }
+ }
+ ciphertext.append((char) c);
+ });
+ return ciphertext.toString();
+ }
+
+ public static class Data extends TestResultAction.Data {
+
+ private Map ciphertextMap;
+
+ public Data(Map ciphertextMap) {
+ this.ciphertextMap = ciphertextMap;
+ }
+
+ @Override
+ @SuppressWarnings("deprecation")
+ public List getTestAction(hudson.tasks.junit.TestObject t) {
+ TestObject testObject = (TestObject) t;
+
+ if (testObject instanceof CaseResult) {
+ return Collections.singletonList(new Rot13CaseAction(ciphertextMap.get(
+ ((CaseResult) testObject).getFullName())));
+ }
+ if (testObject instanceof ClassResult) {
+ return Collections.singletonList(new Rot13ClassAction(ciphertextMap.get(
+ ((ClassResult) testObject).getFullName())));
+ }
+ if (testObject instanceof PackageResult) {
+ return Collections.singletonList(new Rot13PackageAction(ciphertextMap.get(
+ ((PackageResult) testObject).getName())));
+ }
+ if (testObject instanceof TestResult) {
+ return Collections.singletonList(new Rot13TestAction());
+ }
+ return Collections.emptyList();
+ }
+
+ }
+
+ @Extension
+ @Symbol("rot13")
+ public static class DescriptorImpl extends Descriptor {
+
+ @Override
+ public String getDisplayName() {
+ return "ROT13-encoded test case, class and package names";
+ }
+
+ }
+
+}
diff --git a/src/test/java/hudson/tasks/junit/rot13/Rot13TestAction.java b/src/test/java/hudson/tasks/junit/rot13/Rot13TestAction.java
new file mode 100644
index 000000000..4c27664a0
--- /dev/null
+++ b/src/test/java/hudson/tasks/junit/rot13/Rot13TestAction.java
@@ -0,0 +1,25 @@
+package hudson.tasks.junit.rot13;
+
+import hudson.tasks.junit.TestAction;
+
+public class Rot13TestAction extends TestAction {
+
+ public Rot13TestAction() {
+ }
+
+ @Override
+ public final String getIconFileName() {
+ return null;
+ }
+
+ @Override
+ public final String getDisplayName() {
+ return null;
+ }
+
+ @Override
+ public String getUrlName() {
+ return null;
+ }
+
+}
diff --git a/src/test/resources/hudson/tasks/junit/junit-report-494.xml b/src/test/resources/hudson/tasks/junit/junit-report-494.xml
new file mode 100644
index 000000000..c7ddf25c0
--- /dev/null
+++ b/src/test/resources/hudson/tasks/junit/junit-report-494.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+ org.junit.ComparisonFailure: expected: [a] but was: [b]
+
+
+
+
+
diff --git a/src/test/resources/hudson/tasks/junit/rot13/Rot13CaseAction/tablerow.jelly b/src/test/resources/hudson/tasks/junit/rot13/Rot13CaseAction/tablerow.jelly
new file mode 100644
index 000000000..62d45f5b7
--- /dev/null
+++ b/src/test/resources/hudson/tasks/junit/rot13/Rot13CaseAction/tablerow.jelly
@@ -0,0 +1 @@
+${it.ciphertext} for case |
diff --git a/src/test/resources/hudson/tasks/junit/rot13/Rot13ClassAction/casetableheader.jelly b/src/test/resources/hudson/tasks/junit/rot13/Rot13ClassAction/casetableheader.jelly
new file mode 100644
index 000000000..1d23d6efa
--- /dev/null
+++ b/src/test/resources/hudson/tasks/junit/rot13/Rot13ClassAction/casetableheader.jelly
@@ -0,0 +1 @@
+
diff --git a/src/test/resources/hudson/tasks/junit/rot13/Rot13ClassAction/tablerow.jelly b/src/test/resources/hudson/tasks/junit/rot13/Rot13ClassAction/tablerow.jelly
new file mode 100644
index 000000000..ca0a525e3
--- /dev/null
+++ b/src/test/resources/hudson/tasks/junit/rot13/Rot13ClassAction/tablerow.jelly
@@ -0,0 +1 @@
+${it.ciphertext} for class |
diff --git a/src/test/resources/hudson/tasks/junit/rot13/Rot13PackageAction/casetableheader.jelly b/src/test/resources/hudson/tasks/junit/rot13/Rot13PackageAction/casetableheader.jelly
new file mode 100644
index 000000000..5d995987c
--- /dev/null
+++ b/src/test/resources/hudson/tasks/junit/rot13/Rot13PackageAction/casetableheader.jelly
@@ -0,0 +1 @@
+
diff --git a/src/test/resources/hudson/tasks/junit/rot13/Rot13PackageAction/classtableheader.jelly b/src/test/resources/hudson/tasks/junit/rot13/Rot13PackageAction/classtableheader.jelly
new file mode 100644
index 000000000..41348f9cb
--- /dev/null
+++ b/src/test/resources/hudson/tasks/junit/rot13/Rot13PackageAction/classtableheader.jelly
@@ -0,0 +1 @@
+
diff --git a/src/test/resources/hudson/tasks/junit/rot13/Rot13PackageAction/tablerow.jelly b/src/test/resources/hudson/tasks/junit/rot13/Rot13PackageAction/tablerow.jelly
new file mode 100644
index 000000000..483a6ff49
--- /dev/null
+++ b/src/test/resources/hudson/tasks/junit/rot13/Rot13PackageAction/tablerow.jelly
@@ -0,0 +1 @@
+${it.ciphertext} for package |
diff --git a/src/test/resources/hudson/tasks/junit/rot13/Rot13TestAction/casetableheader.jelly b/src/test/resources/hudson/tasks/junit/rot13/Rot13TestAction/casetableheader.jelly
new file mode 100644
index 000000000..cc456567a
--- /dev/null
+++ b/src/test/resources/hudson/tasks/junit/rot13/Rot13TestAction/casetableheader.jelly
@@ -0,0 +1 @@
+
diff --git a/src/test/resources/hudson/tasks/junit/rot13/Rot13TestAction/packagetableheader.jelly b/src/test/resources/hudson/tasks/junit/rot13/Rot13TestAction/packagetableheader.jelly
new file mode 100644
index 000000000..9b77570fb
--- /dev/null
+++ b/src/test/resources/hudson/tasks/junit/rot13/Rot13TestAction/packagetableheader.jelly
@@ -0,0 +1 @@
+