/*******************************************************************************
*
* Copyright (c) 2004-2011 Oracle Corporation.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
*
* Inc., Kohsuke Kawaguchi, Daniel Dyer, Red Hat, Inc., Stephen Connolly, id:cactusman, Yahoo!, Inc, Winston Prakash
*
*
*******************************************************************************/
package hudson.tasks.test;
import hudson.Functions;
import hudson.model.*;
import hudson.tasks.junit.CaseResult;
import hudson.util.*;
import java.awt.Color;
import org.jvnet.localizer.Localizable;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import org.kohsuke.stapler.export.Exported;
import org.kohsuke.stapler.export.ExportedBean;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.eclipse.hudson.graph.*;
import org.eclipse.hudson.graph.ChartUtil.NumberOnlyBuildLabel;
/**
* Common base class for recording test result.
*
* <p> {@link Project} and {@link Build} recognizes {@link Action}s that derive
* from this, and displays it nicely (regardless of the underlying
* implementation.)
*
* @author Kohsuke Kawaguchi
*/
@ExportedBean
public abstract class AbstractTestResultAction<T extends AbstractTestResultAction> implements HealthReportingAction {
//TODO: review and check whether we can do it private
public final AbstractBuild<?, ?> owner;
private Map<String, String> descriptions = new ConcurrentHashMap<String, String>();
protected AbstractTestResultAction(AbstractBuild owner) {
this.owner = owner;
}
/**
* Gets the number of failed tests.
*/
@Exported(visibility = 2)
public abstract int getFailCount();
/**
* Gets the number of skipped tests.
*/
@Exported(visibility = 2)
public int getSkipCount() {
// Not all sub-classes will understand the concept of skipped tests.
// This default implementation is for them, so that they don't have
// to implement it (this avoids breaking existing plug-ins - i.e. those
// written before this method was added in 1.178).
// Sub-classes that do support skipped tests should over-ride this method.
return 0;
}
/**
* Gets the total number of tests.
*/
@Exported(visibility = 2)
public abstract int getTotalCount();
/**
* Gets the diff string of failures.
*/
public final String getFailureDiffString() {
T prev = getPreviousResult();
if (prev == null) {
return ""; // no record
}
return " / " + Functions.getDiffString(this.getFailCount() - prev.getFailCount());
}
public String getDisplayName() {
return Messages.AbstractTestResultAction_getDisplayName();
}
@Exported(visibility = 2)
public String getUrlName() {
return "testReport";
}
public String getIconFileName() {
return "clipboard.png";
}
public AbstractBuild getOwner() {
return owner;
}
public HealthReport getBuildHealth() {
final int totalCount = getTotalCount();
final int failCount = getFailCount();
int score = (totalCount == 0) ? 100 : (int) (100.0 * (1.0 - ((double) failCount) / totalCount));
Localizable description, displayName = Messages._AbstractTestResultAction_getDisplayName();
if (totalCount == 0) {
description = Messages._AbstractTestResultAction_zeroTestDescription(displayName);
} else {
description = Messages._AbstractTestResultAction_TestsDescription(displayName, failCount, totalCount);
}
return new HealthReport(score, description);
}
/**
* Exposes this object to the remote API.
*/
public Api getApi() {
return new Api(this);
}
/**
* Returns the object that represents the actual test result. This method is
* used by the remote API so that the XML/JSON that we are sending won't
* contain unnecessary indirection (that is,
* {@link AbstractTestResultAction} in between.
*
* <p> If such a concept doesn't make sense for a particular subtype, return
* <tt>this</tt>.
*/
public abstract Object getResult();
/**
* Gets the test result of the previous build, if it's recorded, or null.
*/
public T getPreviousResult() {
return (T) getPreviousResult(getClass());
}
private <U extends AbstractTestResultAction> U getPreviousResult(Class<U> type) {
AbstractBuild<?, ?> b = owner;
while (true) {
b = b.getPreviousBuild();
if (b == null) {
return null;
}
U r = b.getAction(type);
if (r != null) {
return r;
}
}
}
public TestResult findPreviousCorresponding(TestResult test) {
T previousResult = getPreviousResult();
if (previousResult != null) {
TestResult testResult = (TestResult) getResult();
return testResult.findCorrespondingResult(test.getId());
}
return null;
}
public TestResult findCorrespondingResult(String id) {
return ((TestResult) getResult()).findCorrespondingResult(id);
}
/**
* A shortcut for summary.jelly
*
* @return List of failed tests from associated test result.
*/
public List<CaseResult> getFailedTests() {
return Collections.emptyList();
}
/**
* {@link TestObject}s do not have their own persistence mechanism, so
* updatable data of {@link TestObject}s need to be persisted by the owning
* {@link AbstractTestResultAction}, and this method and
* {@link #setDescription(TestObject, String)} provides that logic.
*
* <p> The default implementation stores information in the 'this' object.
*
* @see TestObject#getDescription()
*/
protected String getDescription(TestObject object) {
return descriptions.get(object.getId());
}
protected void setDescription(TestObject object, String description) {
descriptions.put(object.getId(), description);
}
public Object readResolve() {
if (descriptions == null) {
descriptions = new ConcurrentHashMap<String, String>();
}
return this;
}
/**
* Returns a full path down to a test result
*/
public String getTestResultPath(TestResult it) {
return getUrlName() + "/" + it.getRelativePathFrom(null);
}
/**
* Generates a PNG image for the test result trend.
*/
public void doGraph(StaplerRequest req, StaplerResponse rsp) throws IOException {
if (ChartUtil.awtProblemCause != null) {
// not available. send out error message
rsp.sendRedirect2(req.getContextPath() + "/images/headless.png");
return;
}
if (req.checkIfModified(owner.getTimestamp(), rsp)) {
return;
}
Area defSize = calcDefaultSize();
Graph graph = new Graph(-1, defSize.width, defSize.height);
graph.setYAxisLabel("count");
graph.setData(getGraphDataSet(req));
graph.doPng(req, rsp);
//ChartUtil.generateGraph(req,rsp,createChart(req,buildDataSet(req)),calcDefaultSize());
}
/**
* Generates a clickable map HTML for
* {@link #doGraph(StaplerRequest, StaplerResponse)}.
*/
public void doGraphMap(StaplerRequest req, StaplerResponse rsp) throws IOException {
if (req.checkIfModified(owner.getTimestamp(), rsp)) {
return;
}
Area defSize = calcDefaultSize();
Graph graph = new Graph(-1, defSize.width, defSize.height);
graph.setYAxisLabel("count");
graph.setData(getGraphDataSet(req));
graph.doMap(req, rsp);
}
private DataSet getGraphDataSet(StaplerRequest req) {
boolean failureOnly = Boolean.valueOf(req.getParameter("failureOnly"));
DataSet<String, ChartLabel> testResultDataSet = new DataSet<String, ChartLabel>();
GraphSeries<String> xSeries = new GraphSeries<String>("Build No.");
testResultDataSet.setXSeries(xSeries);
GraphSeries<Number> ySeriesFailed = new GraphSeries<Number>(GraphSeries.TYPE_AREA, "Failed", ColorPalette.RED);
ySeriesFailed.setBaseURL(getRelPath(req) + "/${buildNo}/testReport");
GraphSeries<Number> ySeriesSkipped = new GraphSeries<Number>(GraphSeries.TYPE_AREA, "Skipped", ColorPalette.YELLOW);
ySeriesSkipped.setBaseURL(getRelPath(req) + "/${buildNo}/testReport");
GraphSeries<Number> ySeriesPassed = new GraphSeries<Number>(GraphSeries.TYPE_AREA, "Passed", ColorPalette.BLUE);
ySeriesPassed.setBaseURL(getRelPath(req) + "/${buildNo}/testReport");
if (!failureOnly) {
testResultDataSet.addYSeries(ySeriesFailed);
testResultDataSet.addYSeries(ySeriesSkipped);
testResultDataSet.addYSeries(ySeriesPassed);
} else {
testResultDataSet.addYSeries(ySeriesFailed);
}
for (AbstractTestResultAction<?> a = this; a != null; a = a.getPreviousResult(AbstractTestResultAction.class)) {
xSeries.add("#" + String.valueOf(a.owner.number));
ySeriesFailed.add((double) a.getFailCount());
// For backward compatibility with JFreechart
testResultDataSet.add((double) a.getFailCount(), "failed", new TestResultChartLabel(req, a.owner));
if (!failureOnly) {
ySeriesSkipped.add((double) a.getSkipCount());
ySeriesPassed.add((double) (a.getTotalCount() - a.getFailCount() - a.getSkipCount()));
// For backward compatibility with JFreechart
testResultDataSet.add((double) a.getSkipCount(), "skipped", new TestResultChartLabel(req, a.owner));
testResultDataSet.add((double) a.getTotalCount() - a.getFailCount() - a.getSkipCount(), "total", new TestResultChartLabel(req, a.owner));
}
}
// We want to display the build result from older to latest
testResultDataSet.reverseOrder();
return testResultDataSet;
}
// For backward compatibility with JFreechart
private class TestResultChartLabel extends NumberOnlyBuildLabel {
final String relPath;
public TestResultChartLabel(StaplerRequest req, AbstractBuild build) {
super(build);
relPath = getRelPath(req);
}
@Override
public Color getColor(int row, int column) {
return ColorPalette.BLUE;
}
@Override
public String getLink(int row, int column) {
return relPath + build.getNumber() + "/testReport/";
}
@Override
public String getToolTip(int row, int column) {
AbstractTestResultAction a = build.getAction(AbstractTestResultAction.class);
switch (row) {
case 0:
return String.valueOf(Messages.AbstractTestResultAction_fail(build.getDisplayName(), a.getFailCount()));
case 1:
return String.valueOf(Messages.AbstractTestResultAction_skip(build.getDisplayName(), a.getSkipCount()));
default:
return String.valueOf(Messages.AbstractTestResultAction_test(build.getDisplayName(), a.getTotalCount()));
}
}
public int compareTo(ChartLabel that) {
return this.build.number - ((TestResultChartLabel) that).build.number;
}
}
/**
* Determines the default size of the trend graph.
*
* This is default because the query parameter can choose arbitrary size. If
* the screen resolution is too low, use a smaller size.
*/
private Area calcDefaultSize() {
Area res = Functions.getScreenResolution();
if (res != null && res.width <= 800) {
return new Area(250, 100);
} else {
return new Area(500, 200);
}
}
private String getRelPath(StaplerRequest req) {
String relPath = req.getParameter("rel");
if (relPath == null) {
return "";
}
return relPath;
}
}