Skip to content

Commit

Permalink
Convert trend to ECharts (#134)
Browse files Browse the repository at this point in the history
  • Loading branch information
timja authored Jul 30, 2020
1 parent bab34bc commit 1f37075
Show file tree
Hide file tree
Showing 8 changed files with 219 additions and 43 deletions.
18 changes: 14 additions & 4 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<parent>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>plugin</artifactId>
<version>4.1</version>
<version>4.3</version>
<relativePath />
</parent>
<artifactId>junit</artifactId>
Expand All @@ -15,7 +15,7 @@
<properties>
<revision>1.30</revision>
<changelist>-SNAPSHOT</changelist>
<jenkins.version>2.164.3</jenkins.version>
<jenkins.version>2.204.4</jenkins.version>
<java.level>8</java.level>
<no-test-jar>false</no-test-jar>
</properties>
Expand Down Expand Up @@ -44,6 +44,11 @@
</pluginRepository>
</pluginRepositories>
<dependencies>
<dependency>
<groupId>io.jenkins.plugins</groupId>
<artifactId>echarts-api</artifactId>
<version>4.7.0-4</version>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>structs</artifactId>
Expand Down Expand Up @@ -113,11 +118,16 @@
<dependencies>
<dependency>
<groupId>io.jenkins.tools.bom</groupId>
<artifactId>bom-2.164.x</artifactId>
<version>9</version>
<artifactId>bom-2.204.x</artifactId>
<version>11</version>
<scope>import</scope>
<type>pom</type>
</dependency>
<dependency>
<groupId>com.github.spotbugs</groupId>
<artifactId>spotbugs-annotations</artifactId>
<version>4.0.2</version>
</dependency>
</dependencies>
</dependencyManagement>
</project>
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@
import org.jfree.data.category.CategoryDataset;
import org.jfree.ui.RectangleInsets;
import org.jvnet.localizer.Localizable;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import org.kohsuke.stapler.export.Exported;
Expand Down Expand Up @@ -213,7 +215,8 @@ public T getPreviousResult() {
return (T)getPreviousResult(getClass(), true);
}

private <U extends AbstractTestResultAction> U getPreviousResult(Class<U> type, boolean eager) {
@Restricted(NoExternalUse.class)
public <U extends AbstractTestResultAction> U getPreviousResult(Class<U> type, boolean eager) {
Run<?,?> b = run;
Set<Integer> loadedBuilds;
if (!eager && run.getParent() instanceof LazyBuildMixIn.LazyLoadingJob) {
Expand Down Expand Up @@ -289,7 +292,10 @@ public List<? extends TestResult> getSkippedTests() {

/**
* Generates a PNG image for the test result trend.
*
* @deprecated Replaced by echarts in TODO
*/
@Deprecated
public void doGraph( StaplerRequest req, StaplerResponse rsp) throws IOException {
if(ChartUtil.awtProblemCause!=null) {
// not available. send out error message
Expand Down
91 changes: 91 additions & 0 deletions src/main/java/hudson/tasks/test/TestResultActionIterable.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package hudson.tasks.test;

import edu.hm.hafner.echarts.Build;
import edu.hm.hafner.echarts.BuildResult;
import edu.umd.cs.findbugs.annotations.NonNull;
import hudson.model.Run;
import java.util.Iterator;
import java.util.NoSuchElementException;

public class TestResultActionIterable implements Iterable<BuildResult<AbstractTestResultAction<?>>> {
private final AbstractTestResultAction<?> latestAction;

/**
* Creates a new iterator that selects action of the given type {@code actionType}.
*
* @param baseline
* the baseline to start from
*/
public TestResultActionIterable(final AbstractTestResultAction<?> baseline) {
this.latestAction = baseline;
}

@NonNull
@Override
public Iterator<BuildResult<AbstractTestResultAction<?>>> iterator() {
if (latestAction == null) {
return new TestResultActionIterator(null);
}
return new TestResultActionIterator(latestAction);
}

private static class TestResultActionIterator implements Iterator<BuildResult<AbstractTestResultAction<?>>> {
private AbstractTestResultAction<?> cursor;
private AbstractTestResultAction<?> initialValue;

/**
* Creates a new iterator starting from the baseline.
*
* @param baseline
* the run to start from
*/
TestResultActionIterator(final AbstractTestResultAction<?> baseline) {
initialValue = baseline;
}

@Override
public boolean hasNext() {
if (initialValue != null) {
return true;
}

if (cursor == null) {
return false;
}

AbstractTestResultAction<?> previousBuild = cursor.getPreviousResult(AbstractTestResultAction.class, true);
return previousBuild != null;
}

@Override
public BuildResult<AbstractTestResultAction<?>> next() {
if (initialValue == null && cursor == null) {
throw new NoSuchElementException(
"There is no action available anymore. Use hasNext() before calling next().");
}
AbstractTestResultAction<?> buildAction = getBuildAction();
if (buildAction != null) {
cursor = buildAction;
Run<?, ?> run = cursor.run;

int buildTimeInSeconds = (int) (run.getTimeInMillis() / 1000);
Build build = new Build(run.getNumber(), run.getDisplayName(), buildTimeInSeconds);
return new BuildResult<>(build, buildAction);
}

throw new NoSuchElementException("No more runs with a test result available: " + cursor);
}

private AbstractTestResultAction<?> getBuildAction() {
AbstractTestResultAction<?> run;
if (initialValue != null) {
run = initialValue;
initialValue = null;
} else {
run = cursor.getPreviousResult(AbstractTestResultAction.class, true);
}
return run;
}
}

}
33 changes: 32 additions & 1 deletion src/main/java/hudson/tasks/test/TestResultProjectAction.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,15 @@
*/
package hudson.tasks.test;

import edu.hm.hafner.echarts.ChartModelConfiguration;
import edu.hm.hafner.echarts.JacksonFacade;
import edu.hm.hafner.echarts.LinesChartModel;
import hudson.model.AbstractProject;
import hudson.model.Action;
import hudson.model.Job;
import hudson.model.Run;
import hudson.tasks.junit.JUnitResultArchiver;
import io.jenkins.plugins.echarts.AsyncTrendChart;
import org.kohsuke.stapler.Ancestor;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
Expand All @@ -37,6 +41,7 @@
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
import org.kohsuke.stapler.bind.JavaScriptMethod;

/**
* Project action object from test reporter, such as {@link JUnitResultArchiver},
Expand All @@ -47,7 +52,7 @@
*
* @author Kohsuke Kawaguchi
*/
public class TestResultProjectAction implements Action {
public class TestResultProjectAction implements Action, AsyncTrendChart {
/**
* Project that owns this action.
* @since 1.2-beta-1
Expand Down Expand Up @@ -102,9 +107,21 @@ public AbstractTestResultAction getLastTestResultAction() {
return null;
}

protected LinesChartModel createChartModel() {
return new TestResultTrendChart().create(createBuildHistory(), new ChartModelConfiguration());
}

public TestResultActionIterable createBuildHistory() {
Run<?, ?> lastCompletedBuild = job.getLastCompletedBuild();
return new TestResultActionIterable(lastCompletedBuild.getAction(AbstractTestResultAction.class));
}

/**
* Display the test result trend.
*
* @deprecated Replaced by echarts in TODO
*/
@Deprecated
public void doTrend( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException {
AbstractTestResultAction a = getLastTestResultAction();
if(a!=null)
Expand All @@ -115,7 +132,10 @@ public void doTrend( StaplerRequest req, StaplerResponse rsp ) throws IOExceptio

/**
* Generates the clickable map HTML fragment for {@link #doTrend(StaplerRequest, StaplerResponse)}.
*
* @deprecated Replaced by echarts in TODO
*/
@Deprecated
public void doTrendMap( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException {
AbstractTestResultAction a = getLastTestResultAction();
if(a!=null)
Expand Down Expand Up @@ -155,4 +175,15 @@ public void doFlipTrend( StaplerRequest req, StaplerResponse rsp ) throws IOExce
}

private static final String FAILURE_ONLY_COOKIE = "TestResultAction_failureOnly";

@JavaScriptMethod
@Override
public String getBuildTrendModel() {
return new JacksonFacade().toJson(createChartModel());
}

@Override
public boolean isTrendVisible() {
return true;
}
}
37 changes: 37 additions & 0 deletions src/main/java/hudson/tasks/test/TestResultTrendChart.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package hudson.tasks.test;

import edu.hm.hafner.echarts.ChartModelConfiguration;
import edu.hm.hafner.echarts.LineSeries;
import edu.hm.hafner.echarts.LinesChartModel;
import edu.hm.hafner.echarts.LinesDataSet;
import edu.hm.hafner.echarts.Palette;

public class TestResultTrendChart {

public LinesChartModel create(final Iterable results,
final ChartModelConfiguration configuration) {
TestResultTrendSeriesBuilder builder = new TestResultTrendSeriesBuilder();
LinesDataSet dataSet = builder.createDataSet(configuration, results);

LinesChartModel model = new LinesChartModel();
model.setDomainAxisLabels(dataSet.getDomainAxisLabels());
model.setBuildNumbers(dataSet.getBuildNumbers());

LineSeries failed = new LineSeries("Failed", Palette.RED.getNormal(),
LineSeries.StackedMode.STACKED, LineSeries.FilledMode.FILLED);
failed.addAll(dataSet.getSeries(TestResultTrendSeriesBuilder.FAILED_KEY));
model.addSeries(failed);

LineSeries skipped = new LineSeries("Skipped", Palette.GRAY.getNormal(),
LineSeries.StackedMode.STACKED, LineSeries.FilledMode.FILLED);
skipped.addAll(dataSet.getSeries(TestResultTrendSeriesBuilder.SKIPPED_KEY));
model.addSeries(skipped);

LineSeries passed = new LineSeries("Passed", Palette.BLUE.getNormal(),
LineSeries.StackedMode.STACKED, LineSeries.FilledMode.FILLED);
passed.addAll(dataSet.getSeries(TestResultTrendSeriesBuilder.PASSED_KEY));
model.addSeries(passed);

return model;
}
}
23 changes: 23 additions & 0 deletions src/main/java/hudson/tasks/test/TestResultTrendSeriesBuilder.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package hudson.tasks.test;

import edu.hm.hafner.echarts.SeriesBuilder;
import hudson.tasks.junit.TestResultAction;
import java.util.HashMap;
import java.util.Map;

public class TestResultTrendSeriesBuilder extends SeriesBuilder<AbstractTestResultAction> {
static final String TOTALS_KEY = "total";
static final String PASSED_KEY = "passed";
static final String FAILED_KEY = "failed";
static final String SKIPPED_KEY = "skipped";

@Override
protected Map<String, Integer> computeSeries(AbstractTestResultAction testResultAction) {
Map<String, Integer> series = new HashMap<>();
series.put(TOTALS_KEY, testResultAction.getTotalCount());
series.put(PASSED_KEY, testResultAction.getPassedTests().size());
series.put(FAILED_KEY, testResultAction.getFailCount());
series.put(SKIPPED_KEY, testResultAction.getSkipCount());
return series;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,32 +23,7 @@ THE SOFTWARE.
-->

<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form" xmlns:i="jelly:fmt" xmlns:local="local">
<j:set var="tr" value="${action.lastTestResultAction}" />
<j:if test="${tr.previousResult!=null}">
<!-- at least two data points are required for a trend report -->
<div align="right">
<j:set var="mode" value="${h.getCookie(request,'TestResultAction_failureOnly').value}" />
<j:if test="${mode!=null}">
<j:set var="trendQueryString1" value="?failureOnly=${mode}" />
<j:set var="trendQueryString2" value="&amp;failureOnly=${mode}" />
</j:if>
<div class="test-trend-caption">
${%Test Result Trend}
</div>
<div>
<img src="test/trend${trendQueryString1}" lazymap="test/trendMap${trendQueryString1}" alt="[Test result trend chart]"/>
</div>
<div style="text-align:right">
<a href="test/flipTrend">
<j:choose>
<!-- needs to strip whitespace here -->
<j:when test="${mode}">(${%show test # and failure #})</j:when>
<j:otherwise>(${%just show failures})</j:otherwise>
</j:choose>
</a> <st:nbsp/>
<a href="test/?width=800&amp;height=600${trendQueryString2}">${%enlarge}</a>
</div>
</div>
</j:if>
</j:jelly>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout"
xmlns:t="/lib/hudson" xmlns:f="/lib/form" xmlns:i="jelly:fmt" xmlns:local="local" xmlns:c="/charts">
<c:trend-chart it="${from}" title="${%Test Result Trend}" enableLinks="true"/>
</j:jelly>
19 changes: 11 additions & 8 deletions src/test/java/hudson/tasks/junit/TestResultPublishingTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,12 @@
import java.io.IOException;
import java.util.concurrent.TimeUnit;

import static org.junit.Assert.*;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.core.Is.is;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

public class TestResultPublishingTest {
@Rule
Expand Down Expand Up @@ -130,15 +135,13 @@ public void testOpenJUnitPublishing() throws IOException, SAXException {
// after "Latest Test Result" it should say "no failures"
rule.assertXPathResultsContainText(projectPage, "//td", "(no failures)");
// there should be a test result trend graph
rule.assertXPath(projectPage, "//img[@src='test/trend']");
// the trend graph should be served up with a good http status
Page trendGraphPage = wc.goTo(proj.getUrl() + "/test/trend", "image/png");
rule.assertGoodStatus(trendGraphPage);
HtmlElement trendGraphCaption = (HtmlElement) projectPage.getByXPath( "//div[@class='test-trend-caption']").get(0);
assertThat(trendGraphCaption.getTextContent(), is("Test Result Trend"));
HtmlElement testCanvas = ((HtmlElement) trendGraphCaption.getParentNode()).getElementsByTagName("canvas").get(0);
assertNotNull("couldn't find test result trend graph", testCanvas);

// The trend graph should be clickable and take us to a run details page
Object imageNode = projectPage.getFirstByXPath("//img[@src='test/trend']");
assertNotNull("couldn't find any matching nodes", imageNode);
assertTrue("image node should be an HtmlImage object", imageNode instanceof HtmlImage);
assertTrue("image node should be an HtmlCanvas object", testCanvas instanceof HtmlCanvas);
// TODO: Check that we can click on the graph and get to a particular run. How do I do this with HtmlUnit?

XmlPage xmlProjectPage = wc.goToXml(proj.getUrl() + "/lastBuild/testReport/api/xml");
Expand Down

0 comments on commit 1f37075

Please sign in to comment.