/*
* The MIT License
*
* Copyright (c) 2011 Bruno P. Kinoshita
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.tap4j.plugin;
import hudson.matrix.MatrixProject;
import hudson.model.AbstractProject;
import hudson.model.Job;
import hudson.model.Run;
import hudson.util.ChartUtil;
import hudson.util.DataSetBuilder;
import hudson.util.RunList;
import org.jfree.chart.JFreeChart;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import org.tap4j.plugin.util.GraphHelper;
import java.io.IOException;
import java.util.Calendar;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
/**
* A TAP Project action, with a graph and a list of builds.
*
* @author Bruno P. Kinoshita - http://www.kinoshita.eti.br
* @since 1.0
*/
public class TapProjectAction extends AbstractTapProjectAction {
protected class Result {
public int numPassed;
public int numFailed;
public int numSkipped;
public Result() {
numPassed = 0;
numFailed = 0;
numSkipped = 0;
}
public void add(Result r) {
numPassed += r.numPassed;
numFailed += r.numFailed;
numSkipped += r.numSkipped;
}
}
/**
* Used to figure out if we need to regenerate the graphs or not. Only used
* in newGraphNotNeeded() method. Key is the request URI and value is the
* number of builds for the project.
*/
private transient Map<String, Integer> requestMap = new HashMap<String, Integer>();
public TapProjectAction(AbstractProject<?, ?> project) {
super(project);
}
public TapProjectAction(Job<?, ?> job) {
super(job);
}
public AbstractProject<?, ?> getProject() {
return this.project;
}
protected Class<TapBuildAction> getBuildActionClass() {
return TapBuildAction.class;
}
public TapBuildAction getLastBuildAction() {
TapBuildAction action = null;
final Run<?, ?> lastBuild = this.getLastBuildWithTap();
if (lastBuild != null) {
action = lastBuild.getAction(TapBuildAction.class);
}
return action;
}
/**
* @return
*/
private Run<?, ?> getLastBuildWithTap() {
Run<?, ?> lastBuild = this.job.getLastBuild();
while (lastBuild != null && lastBuild.getAction(TapBuildAction.class) == null) {
lastBuild = lastBuild.getPreviousBuild();
}
return lastBuild;
}
public void doIndex( final StaplerRequest request,
final StaplerResponse response ) throws IOException {
Run<?, ?> lastBuild = this.getLastBuildWithTap();
if (lastBuild == null) {
response.sendRedirect2("nodata");
} else {
int buildNumber = lastBuild.getNumber();
response.sendRedirect2(String.format("../%d/%s", buildNumber,
TapBuildAction.URL_NAME));
}
}
/**
* Generates the graph that shows test pass/fail ratio.
*
* @param req Stapler request
* @param rsp Stapler response
* @throws IOException if it fails to create the graph image and serve it
*/
public void doGraph( final StaplerRequest req, StaplerResponse rsp ) throws IOException {
if (newGraphNotNeeded(req, rsp)) {
return;
}
final DataSetBuilder<String, ChartUtil.NumberOnlyBuildLabel> dataSetBuilder = new DataSetBuilder<String, ChartUtil.NumberOnlyBuildLabel>();
populateDataSetBuilder(dataSetBuilder);
new hudson.util.Graph(-1, getGraphWidth(), getGraphHeight()) {
protected JFreeChart createGraph() {
return GraphHelper.createChart(req, dataSetBuilder.build());
}
}.doPng(req, rsp);
}
public void doGraphMap( final StaplerRequest req, StaplerResponse rsp ) throws IOException {
if (newGraphNotNeeded(req, rsp)) {
return;
}
final DataSetBuilder<String, ChartUtil.NumberOnlyBuildLabel> dataSetBuilder = new DataSetBuilder<String, ChartUtil.NumberOnlyBuildLabel>();
// TODO: optimize by using cache
populateDataSetBuilder(dataSetBuilder);
new hudson.util.Graph(-1, getGraphWidth(), getGraphHeight()) {
protected JFreeChart createGraph() {
return GraphHelper.createChart(req, dataSetBuilder.build());
}
}.doMap(req, rsp);
}
/**
* Returns <code>true</code> if there is a graph to plot.
*
* @return value for property 'graphAvailable'
*/
public boolean isGraphActive() {
Run<?, ?> build = this.job.getLastBuild();
// in order to have a graph, we must have at least two points.
int numPoints = 0;
while (numPoints < 2) {
if (build == null) {
return false;
}
if( this.job instanceof MatrixProject )
{
MatrixProject mp = (MatrixProject) this.job;
for (Job j : mp.getAllJobs()) {
if (j != mp) { //getAllJobs includes the parent job too, so skip that
Run<?,?> sub = j.getBuild(build.getId());
if(sub != null) {
// Not all builds are on all sub-projects
if (sub.getAction(getBuildActionClass()) != null) {
//data for at least 1 sub-job on this build
numPoints++;
break; // go look at the next build now
}
}
}
}
}
else
{
if (build.getAction(getBuildActionClass()) != null) {
numPoints++;
}
}
build = build.getPreviousBuild();
}
return true;
}
/**
* If number of builds hasn't changed and if checkIfModified() returns true,
* no need to regenerate the graph. Browser should reuse it's cached image
*
* @param req Stapler request
* @param rsp Stapler response
* @return true, if new image does NOT need to be generated, false otherwise
*/
private boolean newGraphNotNeeded( final StaplerRequest req, StaplerResponse rsp ) {
Calendar t = this.job.getLastCompletedBuild().getTimestamp();
Integer prevNumBuilds = requestMap.get(req.getRequestURI());
int numBuilds = 0;
RunList<?> builds = this.job.getBuilds();
Iterator<?> it = builds.iterator();
while (it.hasNext()) {
it.next();
numBuilds += 1;
}
prevNumBuilds = prevNumBuilds == null ? 0 : prevNumBuilds;
if (prevNumBuilds != numBuilds) {
requestMap.put(req.getRequestURI(), numBuilds);
}
if (requestMap.keySet().size() > 10) {
// keep map size in check
requestMap.clear();
}
if (prevNumBuilds == numBuilds && req.checkIfModified(t, rsp)) {
/*
* checkIfModified() is after '&&' because we want it evaluated only
* if number of builds is different
*/
return true;
}
return false;
}
protected void populateDataSetBuilder(DataSetBuilder<String, ChartUtil.NumberOnlyBuildLabel> dataset ) {
Job<?, ?> p = this.job;
for (Run<?, ?> build = this.job.getLastBuild(); build != null; build = build.getPreviousBuild()) {
/*
* The build has most likely failed before any TAP data was recorded.
*
* If we don't exclude such builds, we'd have to account for that in GraphHelper. Besides that, it's not
* consistent with JUnit graph behaviour where builds without test results are not included in graph.
*/
if (build.getAction(TapBuildAction.class) == null) {
continue;
}
ChartUtil.NumberOnlyBuildLabel label = new ChartUtil.NumberOnlyBuildLabel((Run) build);
Result r = new Result();
if( p instanceof MatrixProject )
{
MatrixProject mp = (MatrixProject) p;
for (Job j : mp.getAllJobs()) {
if (j != mp) { //getAllJobs includes the parent job too, so skip that
Run<?,?> sub = j.getBuild(build.getId());
if(sub != null) {
// Not all builds are on all sub-projects
r.add(summarizeBuild(sub));
}
}
}
}
else
{
r = summarizeBuild(build);
}
dataset.add(r.numPassed, "Passed", label);
dataset.add(r.numFailed, "Failed", label);
dataset.add(r.numSkipped, "Skipped", label);
}
}
protected Result summarizeBuild(Run<?,?> b)
{
Result r = new Result();
TapBuildAction action = b.getAction(getBuildActionClass());
if (action != null) {
TapResult report = action.getResult();
report.tally();
r.numPassed = report.getPassed();
r.numFailed = report.getFailed();
r.numSkipped = report.getSkipped();
}
return r;
}
/**
* Getter for property 'graphWidth'.
*
* @return Value for property 'graphWidth'.
*/
public int getGraphWidth() {
return 500;
}
/**
* Getter for property 'graphHeight'.
*
* @return Value for property 'graphHeight'.
*/
public int getGraphHeight() {
return 200;
}
}