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. ${%Test name} ${%Duration} ${%Status} + + + @@ -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. + + +
    ${%Test Name} ${%Duration} ${%Age}
    ${f.age}
    @@ -64,6 +70,9 @@ THE SOFTWARE. (${%diff}) ${%Total} (${%diff}) + + + @@ -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 @@ +ROT13 for cases on class page 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 @@ +ROT13 for failed cases on package page 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 @@ +ROT13 for all classes on package page 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 @@ +ROT13 for failed cases on test page 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 @@ +ROT13 for all packages on test page