package hudson.plugins.testng; import hudson.Functions; import hudson.model.AbstractBuild; import java.io.IOException; import java.util.Calendar; import hudson.model.Job; import hudson.model.ProminentProjectAction; import hudson.model.Result; import hudson.model.Run; import hudson.plugins.testng.util.GraphHelper; import hudson.tasks.test.TestResultProjectAction; import hudson.util.ChartUtil; import hudson.util.DataSetBuilder; import java.util.SortedMap; import jenkins.model.lazy.LazyBuildMixIn; import jenkins.model.lazy.LazyBuildMixIn.LazyLoadingJob; import org.jfree.chart.JFreeChart; import org.kohsuke.stapler.Stapler; import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerResponse; /** * Action to associate the TestNG reports with the project * * @author nullin */ public class TestNGProjectAction extends TestResultProjectAction implements ProminentProjectAction { private transient boolean escapeTestDescp; private transient boolean escapeExceptionMsg; private transient boolean showFailedBuilds; public TestNGProjectAction(Job<?, ?> project, boolean escapeTestDescp, boolean escapeExceptionMsg, boolean showFailedBuilds) { super(project); this.escapeExceptionMsg = escapeExceptionMsg; this.escapeTestDescp = escapeTestDescp; this.showFailedBuilds = showFailedBuilds; } protected Class<TestNGTestResultBuildAction> getBuildActionClass() { return TestNGTestResultBuildAction.class; } public boolean getEscapeTestDescp() { return escapeTestDescp; } public boolean getEscapeExceptionMsg() { return escapeExceptionMsg; } /** * Getter for property 'project'. * * @return Value for property 'project'. */ public Job<?, ?> getProject() { return super.job; } /** * {@inheritDoc} */ public String getIconFileName() { return PluginImpl.ICON_FILE_NAME; } /** * {@inheritDoc} */ public String getDisplayName() { return PluginImpl.DISPLAY_NAME; } /** * Getter for property 'graphName'. * * @return Value for property 'graphName'. */ public String getGraphName() { return PluginImpl.GRAPH_NAME; } /** * {@inheritDoc} */ public String getUrlName() { return PluginImpl.URL; } /** * {@inheritDoc} */ public String getSearchUrl() { return PluginImpl.URL; } /** * Generates the graph that shows test pass/fail ratio * @param req - * @param rsp - * @throws IOException - */ 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); } /** Generalizes {@link AbstractBuild#getUpUrl} to {@link Run}. */ public String getUpUrl() { return Functions.getNearestAncestorUrl(Stapler.getCurrentRequest(), job) + '/'; } /** * If the last build is the same, * no need to regenerate the graph. Browser should reuse it's cached image * * @param req request * @param rsp response * @return true, if new image does NOT need to be generated, false otherwise */ private boolean newGraphNotNeeded(final StaplerRequest req, StaplerResponse rsp) { Calendar t = getProject().getLastCompletedBuild().getTimestamp(); return req.checkIfModified(t, 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 = getProject().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 (build.getAction(getBuildActionClass()) != null) { numPoints++; } build = build.getPreviousBuild(); } return true; } public TestNGTestResultBuildAction getLastCompletedBuildAction() { for (Run<?, ?> build = getProject().getLastCompletedBuild(); build != null; build = build.getPreviousCompletedBuild()) { final TestNGTestResultBuildAction action = build.getAction(getBuildActionClass()); if (action != null) { return action; } } return null; } protected void populateDataSetBuilder(DataSetBuilder<String, ChartUtil.NumberOnlyBuildLabel> dataset) { if (!(job instanceof LazyBuildMixIn.LazyLoadingJob)) { return; } // cf. AbstractTestResultAction.getPreviousResult(Class, false) SortedMap<Integer, Run<?, ?>> loadedBuilds = (SortedMap<Integer, Run<?, ?>>) ((LazyLoadingJob<?,?>) job).getLazyBuildMixIn()._getRuns().getLoadedBuilds(); for (Run<?, ?> build : loadedBuilds.values()) { ChartUtil.NumberOnlyBuildLabel label = new ChartUtil.NumberOnlyBuildLabel(build); TestNGTestResultBuildAction action = build.getAction(getBuildActionClass()); Result result = build.getResult(); if (result == null || result.isWorseThan(Result.FAILURE)) { //We don't want to add aborted or builds with no results into the graph continue; } if (!showFailedBuilds && result.equals(Result.FAILURE)) { //failed build and configuration states that we should skip this build continue; } if (action != null) { dataset.add(action.getTotalCount() - action.getFailCount() - action.getSkipCount(), "Passed", label); dataset.add(action.getFailCount(), "Failed", label); dataset.add(action.getSkipCount(), "Skipped", label); } } } /** * 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; } }