提交 16e206da 编写于 作者: C CloudBees DEV@Cloud

Merge commit '16197ea5'

......@@ -55,7 +55,9 @@ Upcoming changes</a>
<!-- Record your changes in the trunk here. -->
<div id="trunk" style="display:none"><!--=TRUNK-BEGIN=-->
<ul class=image>
<li class=>
<li class="major rfe">
Moved JUnit reporting functionality to a plugin.
(<a href="https://issues.jenkins-ci.org/browse/JENKINS-23263">issue 23263</a>)
</ul>
</div><!--=TRUNK-END=-->
......
......@@ -298,7 +298,8 @@ public class ClassicPluginStrategy implements PluginStrategy {
new DetachedPlugin("matrix-auth","1.535.*","1.0.2"),
new DetachedPlugin("windows-slaves","1.547.*","1.0"),
new DetachedPlugin("antisamy-markup-formatter","1.553.*","1.0"),
new DetachedPlugin("matrix-project","1.561.*","1.0")
new DetachedPlugin("matrix-project","1.561.*","1.0"),
new DetachedPlugin("junit","1.577.*","1.0")
);
/**
......
......@@ -54,8 +54,6 @@ import hudson.tasks.BuildWrapper;
import hudson.tasks.Builder;
import hudson.tasks.Fingerprinter.FingerprintAction;
import hudson.tasks.Publisher;
import hudson.tasks.test.AbstractTestResultAction;
import hudson.tasks.test.AggregatedTestResultAction;
import hudson.util.*;
import jenkins.model.Jenkins;
import org.kohsuke.stapler.HttpResponse;
......@@ -1041,15 +1039,23 @@ public abstract class AbstractBuild<P extends AbstractProject<P,R>,R extends Abs
/**
* @deprecated Use {@link #getAction(Class)} on {@link AbstractTestResultAction}.
*/
public AbstractTestResultAction getTestResultAction() {
return getAction(AbstractTestResultAction.class);
public Action getTestResultAction() {
try {
return getAction(Jenkins.getInstance().getPluginManager().uberClassLoader.loadClass("hudson.tasks.test.AbstractTestResultAction").asSubclass(Action.class));
} catch (ClassNotFoundException x) {
return null;
}
}
/**
* @deprecated Use {@link #getAction(Class)} on {@link AggregatedTestResultAction}.
*/
public AggregatedTestResultAction getAggregatedTestResultAction() {
return getAction(AggregatedTestResultAction.class);
public Action getAggregatedTestResultAction() {
try {
return getAction(Jenkins.getInstance().getPluginManager().uberClassLoader.loadClass("hudson.tasks.test.AggregatedTestResultAction").asSubclass(Action.class));
} catch (ClassNotFoundException x) {
return null;
}
}
/**
......
......@@ -24,7 +24,6 @@
package hudson.model;
import hudson.Functions;
import hudson.tasks.test.TestResultProjectAction;
/**
* Object that contributes additional information, behaviors, and UIs to {@link ModelObject}
......@@ -46,7 +45,7 @@ import hudson.tasks.test.TestResultProjectAction;
* it will be displayed as a floating box on the top page of
* the target {@link ModelObject}. (For example, this is how
* the JUnit test result trend shows up in the project top page.
* See {@link TestResultProjectAction}.)
* See {@code TestResultProjectAction}.)
*
* <p>
* On the target {@link ModelObject} page, actions are rendered as an item in the side panel
......
......@@ -26,7 +26,6 @@ package hudson.model;
import hudson.tasks.BuildStep;
import hudson.tasks.Recorder;
import hudson.tasks.Builder;
import hudson.tasks.junit.JUnitResultArchiver;
import hudson.scm.SCM;
import javax.annotation.Nonnull;
......@@ -57,7 +56,7 @@ import javax.annotation.Nonnull;
*
* <h2>Example</h2>
* <p>
* {@link JUnitResultArchiver} provides a good example of how a {@link Recorder} can
* {@code JUnitResultArchiver} provides a good example of how a {@link Recorder} can
* depend on its earlier result.
*
* @author Kohsuke Kawaguchi
......@@ -127,9 +126,9 @@ public final class CheckPoint {
*
* <ol>
* <li>Build #1, #2, and #3 happens around the same time
* <li>Build #3 waits for check point {@link JUnitResultArchiver}
* <li>Build #3 waits for check point {@code JUnitResultArchiver}
* <li>Build #2 aborts before getting to that check point
* <li>Build #1 finally checks in {@link JUnitResultArchiver}
* <li>Build #1 finally checks in {@code JUnitResultArchiver}
* </ol>
*
* <p>
......
/*
* The MIT License
*
* Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, Daniel Dyer, id:cactusman, Tom Huybrechts, Yahoo!, Inc.
*
* 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 hudson.tasks.junit;
import hudson.model.AbstractBuild;
import hudson.tasks.test.TabulatedResult;
import hudson.tasks.test.TestResult;
import hudson.tasks.test.TestObject;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import org.kohsuke.stapler.export.Exported;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* Cumulative test result of a test class.
*
* @author Kohsuke Kawaguchi
*/
public final class ClassResult extends TabulatedResult implements Comparable<ClassResult> {
private final String className; // simple name
private transient String safeName;
private final List<CaseResult> cases = new ArrayList<CaseResult>();
private int passCount,failCount,skipCount;
private float duration;
private final PackageResult parent;
ClassResult(PackageResult parent, String className) {
this.parent = parent;
this.className = className;
}
@Override
public AbstractBuild<?, ?> getOwner() {
return (parent==null ? null: parent.getOwner());
}
public PackageResult getParent() {
return parent;
}
@Override
public ClassResult getPreviousResult() {
if(parent==null) return null;
TestResult pr = parent.getPreviousResult();
if(pr==null) return null;
if(pr instanceof PackageResult) {
return ((PackageResult)pr).getClassResult(getName());
}
return null;
}
@Override
public hudson.tasks.test.TestResult findCorrespondingResult(String id) {
String myID = safe(getName());
String caseName = id;
int base = id.indexOf(myID);
if (base > 0) {
int caseNameStart = base + myID.length() + 1;
if (id.length() > caseNameStart) {
caseName = id.substring(caseNameStart);
}
}
CaseResult child = getCaseResult(caseName);
if (child != null) {
return child;
}
return null;
}
public String getTitle() {
return Messages.ClassResult_getTitle(getDisplayName());
}
@Override
public String getChildTitle() {
return "Class Reults";
}
@Exported(visibility=999)
public String getName() {
int idx = className.lastIndexOf('.');
if(idx<0) return className;
else return className.substring(idx+1);
}
public @Override synchronized String getSafeName() {
if (safeName != null) {
return safeName;
}
return safeName = uniquifyName(parent.getChildren(), safe(getName()));
}
public CaseResult getCaseResult(String name) {
for (CaseResult c : cases) {
if(c.getSafeName().equals(name))
return c;
}
return null;
}
@Override
public Object getDynamic(String name, StaplerRequest req, StaplerResponse rsp) {
CaseResult c = getCaseResult(name);
if (c != null) {
return c;
} else {
return super.getDynamic(name, req, rsp);
}
}
@Exported(name="child")
public List<CaseResult> getChildren() {
return cases;
}
public boolean hasChildren() {
return ((cases != null) && (cases.size() > 0));
}
// TODO: wait for stapler 1.60 @Exported
public float getDuration() {
return duration;
}
@Exported
public int getPassCount() {
return passCount;
}
@Exported
public int getFailCount() {
return failCount;
}
@Exported
public int getSkipCount() {
return skipCount;
}
public void add(CaseResult r) {
cases.add(r);
}
/**
* Recount my children.
*/
@Override
public void tally() {
passCount=failCount=skipCount=0;
duration=0;
for (CaseResult r : cases) {
r.setClass(this);
if (r.isSkipped()) {
skipCount++;
}
else if(r.isPassed()) {
passCount++;
}
else {
failCount++;
}
duration += r.getDuration();
}
}
void freeze() {
passCount=failCount=skipCount=0;
duration=0;
for (CaseResult r : cases) {
r.setClass(this);
if (r.isSkipped()) {
skipCount++;
}
else if(r.isPassed()) {
passCount++;
}
else {
failCount++;
}
duration += r.getDuration();
}
Collections.sort(cases);
}
public String getClassName() {
return className;
}
public int compareTo(ClassResult that) {
return this.className.compareTo(that.className);
}
public String getDisplayName() {
return TestNameTransformer.getTransformedName(getName());
}
/**
* @since 1.515
*/
@Override
public String getFullName() {
return getParent().getName() + "." + className;
}
public String getFullDisplayName() {
return getParent().getDisplayName() + "." + TestNameTransformer.getTransformedName(className);
}
/**
* Gets the relative path to this test case from the given object.
*/
@Override
public String getRelativePathFrom(TestObject it) {
if(it instanceof CaseResult) {
return "..";
} else {
return super.getRelativePathFrom(it);
}
}
}
/*
* The MIT License
*
* Copyright (c) 2004-2010, Sun Microsystems, Inc., Tom Huybrechts, Yahoo!, Inc., Seiji Sogabe
*
* 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 hudson.tasks.junit;
import hudson.model.AbstractBuild;
import jenkins.model.Jenkins;
import hudson.tasks.test.TestObject;
import hudson.tasks.test.TestResult;
import hudson.util.ChartUtil;
import hudson.util.ColorPalette;
import hudson.util.DataSetBuilder;
import hudson.util.Graph;
import hudson.util.ShiftedCategoryAxis;
import hudson.util.StackedAreaRenderer2;
import java.awt.Color;
import java.awt.Paint;
import java.util.ArrayList;
import java.util.List;
import org.jfree.chart.ChartFactory;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.axis.CategoryAxis;
import org.jfree.chart.axis.CategoryLabelPositions;
import org.jfree.chart.axis.NumberAxis;
import org.jfree.chart.plot.CategoryPlot;
import org.jfree.chart.plot.PlotOrientation;
import org.jfree.chart.renderer.category.StackedAreaRenderer;
import org.jfree.data.category.CategoryDataset;
import org.jfree.ui.RectangleInsets;
import org.kohsuke.stapler.Stapler;
/**
* History of {@link hudson.tasks.test.TestObject} over time.
*
* @since 1.320
*/
public class History {
private final TestObject testObject;
public History(TestObject testObject) {
this.testObject = testObject;
}
public TestObject getTestObject() {
return testObject;
}
public boolean historyAvailable() {
if (testObject.getOwner().getParent().getBuilds().size() > 1)
return true;
else
return false;
}
public List<TestResult> getList(int start, int end) {
List<TestResult> list = new ArrayList<TestResult>();
end = Math.min(end, testObject.getOwner().getParent().getBuilds().size());
for (AbstractBuild<?,?> b: testObject.getOwner().getParent().getBuilds().subList(start, end)) {
if (b.isBuilding()) continue;
TestResult o = testObject.getResultInBuild(b);
if (o != null) {
list.add(o);
}
}
return list;
}
public List<TestResult> getList() {
return getList(0, testObject.getOwner().getParent().getBuilds().size());
}
/**
* Graph of duration of tests over time.
*/
public Graph getDurationGraph() {
return new GraphImpl("seconds") {
protected DataSetBuilder<String, ChartLabel> createDataSet() {
DataSetBuilder<String, ChartLabel> data = new DataSetBuilder<String, ChartLabel>();
List<TestResult> list;
try {
list = getList(
Integer.parseInt(Stapler.getCurrentRequest().getParameter("start")),
Integer.parseInt(Stapler.getCurrentRequest().getParameter("end")));
} catch (NumberFormatException e) {
list = getList();
}
for (hudson.tasks.test.TestResult o: list) {
data.add(((double) o.getDuration()) / (1000), "", new ChartLabel(o) {
@Override
public Color getColor() {
if (o.getFailCount() > 0)
return ColorPalette.RED;
else if (o.getSkipCount() > 0)
return ColorPalette.YELLOW;
else
return ColorPalette.BLUE;
}
});
}
return data;
}
};
}
/**
* Graph of # of tests over time.
*/
public Graph getCountGraph() {
return new GraphImpl("") {
protected DataSetBuilder<String, ChartLabel> createDataSet() {
DataSetBuilder<String, ChartLabel> data = new DataSetBuilder<String, ChartLabel>();
List<TestResult> list;
try {
list = getList(
Integer.parseInt(Stapler.getCurrentRequest().getParameter("start")),
Integer.parseInt(Stapler.getCurrentRequest().getParameter("end")));
} catch (NumberFormatException e) {
list = getList();
}
for (TestResult o: list) {
data.add(o.getPassCount(), "2Passed", new ChartLabel(o));
data.add(o.getFailCount(), "1Failed", new ChartLabel(o));
data.add(o.getSkipCount(), "0Skipped", new ChartLabel(o));
}
return data;
}
};
}
private abstract class GraphImpl extends Graph {
private final String yLabel;
protected GraphImpl(String yLabel) {
super(-1,600,300); // cannot use timestamp, since ranges may change
this.yLabel = yLabel;
}
protected abstract DataSetBuilder<String, ChartLabel> createDataSet();
protected JFreeChart createGraph() {
final CategoryDataset dataset = createDataSet().build();
final JFreeChart chart = ChartFactory.createStackedAreaChart(null, // chart
// title
null, // unused
yLabel, // range axis label
dataset, // data
PlotOrientation.VERTICAL, // orientation
false, // include legend
true, // tooltips
false // urls
);
chart.setBackgroundPaint(Color.white);
final CategoryPlot plot = chart.getCategoryPlot();
// plot.setAxisOffset(new Spacer(Spacer.ABSOLUTE, 5.0, 5.0, 5.0, 5.0));
plot.setBackgroundPaint(Color.WHITE);
plot.setOutlinePaint(null);
plot.setForegroundAlpha(0.8f);
// plot.setDomainGridlinesVisible(true);
// plot.setDomainGridlinePaint(Color.white);
plot.setRangeGridlinesVisible(true);
plot.setRangeGridlinePaint(Color.black);
CategoryAxis domainAxis = new ShiftedCategoryAxis(null);
plot.setDomainAxis(domainAxis);
domainAxis.setCategoryLabelPositions(CategoryLabelPositions.UP_90);
domainAxis.setLowerMargin(0.0);
domainAxis.setUpperMargin(0.0);
domainAxis.setCategoryMargin(0.0);
final NumberAxis rangeAxis = (NumberAxis) plot.getRangeAxis();
ChartUtil.adjustChebyshev(dataset, rangeAxis);
rangeAxis.setStandardTickUnits(NumberAxis.createIntegerTickUnits());
rangeAxis.setAutoRange(true);
StackedAreaRenderer ar = new StackedAreaRenderer2() {
@Override
public Paint getItemPaint(int row, int column) {
ChartLabel key = (ChartLabel) dataset.getColumnKey(column);
if (key.getColor() != null) return key.getColor();
return super.getItemPaint(row, column);
}
@Override
public String generateURL(CategoryDataset dataset, int row,
int column) {
ChartLabel label = (ChartLabel) dataset.getColumnKey(column);
return label.getUrl();
}
@Override
public String generateToolTip(CategoryDataset dataset, int row,
int column) {
ChartLabel label = (ChartLabel) dataset.getColumnKey(column);
return label.o.getOwner().getDisplayName() + " : "
+ label.o.getDurationString();
}
};
plot.setRenderer(ar);
ar.setSeriesPaint(0,ColorPalette.YELLOW); // Skips.
ar.setSeriesPaint(1,ColorPalette.RED); // Failures.
ar.setSeriesPaint(2,ColorPalette.BLUE); // Total.
// crop extra space around the graph
plot.setInsets(new RectangleInsets(0, 0, 0, 5.0));
return chart;
}
}
class ChartLabel implements Comparable<ChartLabel> {
TestResult o;
String url;
public ChartLabel(TestResult o) {
this.o = o;
this.url = null;
}
public String getUrl() {
if (this.url == null) generateUrl();
return url;
}
private void generateUrl() {
AbstractBuild<?,?> build = o.getOwner();
String buildLink = build.getUrl();
String actionUrl = o.getTestResultAction().getUrlName();
this.url = Jenkins.getInstance().getRootUrl() + buildLink + actionUrl + o.getUrl();
}
public int compareTo(ChartLabel that) {
return this.o.getOwner().number - that.o.getOwner().number;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof ChartLabel)) {
return false;
}
ChartLabel that = (ChartLabel) o;
return this.o == that.o;
}
public Color getColor() {
return null;
}
@Override
public int hashCode() {
return o.hashCode();
}
@Override
public String toString() {
String l = o.getOwner().getDisplayName();
String s = o.getOwner().getBuiltOnStr();
if (s != null)
l += ' ' + s;
return l;
// return o.getDisplayName() + " " + o.getOwner().getDisplayName();
}
}
public static int asInt(String s, int defalutValue) {
if (s==null) return defalutValue;
try {
return Integer.parseInt(s);
} catch (NumberFormatException e) {
return defalutValue;
}
}
}
/*
* The MIT License
*
* Copyright (c) 2009, Yahoo!, Inc.
*
* 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 hudson.tasks.junit;
import hudson.model.TaskListener;
import hudson.tasks.test.TestResultParser;
import hudson.model.AbstractBuild;
import hudson.*;
import hudson.remoting.VirtualChannel;
import java.io.IOException;
import java.io.File;
import org.apache.tools.ant.types.FileSet;
import org.apache.tools.ant.DirectoryScanner;
/**
* Parse some JUnit xml files and generate a TestResult containing all the
* results parsed.
*/
@Extension
public class JUnitParser extends TestResultParser {
private final boolean keepLongStdio;
/** TODO TestResultParser.all does not seem to ever be called so why must this be an Extension? */
@Deprecated
public JUnitParser() {
this(false);
}
/**
* @param keepLongStdio if true, retain a suite's complete stdout/stderr even if this is huge and the suite passed
* @since 1.358
*/
public JUnitParser(boolean keepLongStdio) {
this.keepLongStdio = keepLongStdio;
}
@Override
public String getDisplayName() {
return Messages.JUnitParser_DisplayName();
}
@Override
public String getTestResultLocationMessage() {
return Messages.JUnitParser_TestResultLocationMessage();
}
@Override
public TestResult parse(String testResultLocations,
AbstractBuild build, Launcher launcher,
TaskListener listener)
throws InterruptedException, IOException
{
final long buildTime = build.getTimestamp().getTimeInMillis();
final long timeOnMaster = System.currentTimeMillis();
// [BUG 3123310] TODO - Test Result Refactor: review and fix TestDataPublisher/TestAction subsystem]
// also get code that deals with testDataPublishers from JUnitResultArchiver.perform
FilePath workspace = build.getWorkspace();
if (workspace == null) {
throw new AbortException(Messages.JUnitParser_no_workspace_found(build));
}
return workspace.act(new ParseResultCallable(testResultLocations, buildTime, timeOnMaster, keepLongStdio));
}
private static final class ParseResultCallable implements
FilePath.FileCallable<TestResult> {
private final long buildTime;
private final String testResults;
private final long nowMaster;
private final boolean keepLongStdio;
private ParseResultCallable(String testResults, long buildTime, long nowMaster, boolean keepLongStdio) {
this.buildTime = buildTime;
this.testResults = testResults;
this.nowMaster = nowMaster;
this.keepLongStdio = keepLongStdio;
}
public TestResult invoke(File ws, VirtualChannel channel) throws IOException {
final long nowSlave = System.currentTimeMillis();
FileSet fs = Util.createFileSet(ws, testResults);
DirectoryScanner ds = fs.getDirectoryScanner();
String[] files = ds.getIncludedFiles();
if (files.length == 0) {
// no test result. Most likely a configuration
// error or fatal problem
throw new AbortException(Messages.JUnitResultArchiver_NoTestReportFound());
}
TestResult result = new TestResult(buildTime + (nowSlave - nowMaster), ds, keepLongStdio);
result.tally();
return result;
}
}
}
/*
* The MIT License
*
* Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, Martin Eigenbrodt,
* Tom Huybrechts, Yahoo!, Inc., Richard Hierlmeier
*
* 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 hudson.tasks.junit;
import hudson.AbortException;
import hudson.Extension;
import hudson.FilePath;
import hudson.Launcher;
import hudson.model.AbstractBuild;
import hudson.model.AbstractProject;
import hudson.model.Action;
import hudson.model.BuildListener;
import hudson.model.Descriptor;
import hudson.model.Result;
import hudson.model.Saveable;
import hudson.tasks.BuildStepDescriptor;
import hudson.tasks.BuildStepMonitor;
import hudson.tasks.Publisher;
import hudson.tasks.Recorder;
import hudson.tasks.junit.TestResultAction.Data;
import hudson.tasks.test.TestResultProjectAction;
import hudson.util.DescribableList;
import hudson.util.FormValidation;
import net.sf.json.JSONObject;
import org.apache.tools.ant.DirectoryScanner;
import org.apache.tools.ant.types.FileSet;
import org.kohsuke.stapler.AncestorInPath;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.QueryParameter;
import org.kohsuke.stapler.StaplerRequest;
import java.io.IOException;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
/**
* Generates HTML report from JUnit test result XML files.
*
* @author Kohsuke Kawaguchi
*/
public class JUnitResultArchiver extends Recorder {
/**
* {@link FileSet} "includes" string, like "foo/bar/*.xml"
*/
private final String testResults;
/**
* If true, retain a suite's complete stdout/stderr even if this is huge and the suite passed.
* @since 1.358
*/
private final boolean keepLongStdio;
/**
* {@link TestDataPublisher}s configured for this archiver, to process the recorded data.
* For compatibility reasons, can be null.
* @since 1.320
*/
private final DescribableList<TestDataPublisher, Descriptor<TestDataPublisher>> testDataPublishers;
private final Double healthScaleFactor;
/**
* left for backwards compatibility
* @deprecated since 2009-08-09.
*/
@Deprecated
public JUnitResultArchiver(String testResults) {
this(testResults, false, null);
}
@Deprecated
public JUnitResultArchiver(String testResults,
DescribableList<TestDataPublisher, Descriptor<TestDataPublisher>> testDataPublishers) {
this(testResults, false, testDataPublishers);
}
@Deprecated
public JUnitResultArchiver(
String testResults,
boolean keepLongStdio,
DescribableList<TestDataPublisher, Descriptor<TestDataPublisher>> testDataPublishers) {
this(testResults, keepLongStdio, testDataPublishers, 1.0);
}
@DataBoundConstructor
public JUnitResultArchiver(
String testResults,
boolean keepLongStdio,
DescribableList<TestDataPublisher, Descriptor<TestDataPublisher>> testDataPublishers,
double healthScaleFactor) {
this.testResults = testResults;
this.keepLongStdio = keepLongStdio;
this.testDataPublishers = testDataPublishers;
this.healthScaleFactor = Math.max(0.0,healthScaleFactor);
}
/**
* In progress. Working on delegating the actual parsing to the JUnitParser.
*/
protected TestResult parse(String expandedTestResults, AbstractBuild build, Launcher launcher, BuildListener listener)
throws IOException, InterruptedException
{
return new JUnitParser(isKeepLongStdio()).parse(expandedTestResults, build, launcher, listener);
}
@Override
public boolean perform(AbstractBuild build, Launcher launcher,
BuildListener listener) throws InterruptedException, IOException {
listener.getLogger().println(Messages.JUnitResultArchiver_Recording());
TestResultAction action;
final String testResults = build.getEnvironment(listener).expand(this.testResults);
try {
TestResult result = parse(testResults, build, launcher, listener);
try {
// TODO can the build argument be omitted now, or is it used prior to the call to addAction?
action = new TestResultAction(build, result, listener);
} catch (NullPointerException npe) {
throw new AbortException(Messages.JUnitResultArchiver_BadXML(testResults));
}
action.setHealthScaleFactor(getHealthScaleFactor()); // TODO do we want to move this to the constructor?
result.freeze(action);
if (result.isEmpty()) {
// most likely a configuration error in the job - e.g. false pattern to match the JUnit result files
throw new AbortException(Messages.JUnitResultArchiver_ResultIsEmpty());
}
// TODO: Move into JUnitParser [BUG 3123310]
List<Data> data = new ArrayList<Data>();
if (testDataPublishers != null) {
for (TestDataPublisher tdp : testDataPublishers) {
Data d = tdp.getTestData(build, launcher, listener, result);
if (d != null) {
data.add(d);
}
}
}
action.setData(data);
} catch (AbortException e) {
if (build.getResult() == Result.FAILURE)
// most likely a build failed before it gets to the test phase.
// don't report confusing error message.
return true;
listener.getLogger().println(e.getMessage());
build.setResult(Result.FAILURE);
return true;
} catch (IOException e) {
e.printStackTrace(listener.error("Failed to archive test reports"));
build.setResult(Result.FAILURE);
return true;
}
build.addAction(action);
if (action.getResult().getFailCount() > 0)
build.setResult(Result.UNSTABLE);
return true;
}
/**
* Not actually used, but left for backward compatibility
*
* @deprecated since 2009-08-10.
*/
protected TestResult parseResult(DirectoryScanner ds, long buildTime)
throws IOException {
return new TestResult(buildTime, ds);
}
public BuildStepMonitor getRequiredMonitorService() {
return BuildStepMonitor.NONE;
}
public String getTestResults() {
return testResults;
}
public double getHealthScaleFactor() {
return healthScaleFactor == null ? 1.0 : healthScaleFactor;
}
public DescribableList<TestDataPublisher, Descriptor<TestDataPublisher>> getTestDataPublishers() {
return testDataPublishers;
}
@Override
public Collection<Action> getProjectActions(AbstractProject<?, ?> project) {
return Collections.<Action>singleton(new TestResultProjectAction(project));
}
/**
* @return the keepLongStdio
*/
public boolean isKeepLongStdio() {
return keepLongStdio;
}
private static final long serialVersionUID = 1L;
@Extension
public static class DescriptorImpl extends BuildStepDescriptor<Publisher> {
public String getDisplayName() {
return Messages.JUnitResultArchiver_DisplayName();
}
@Override
public String getHelpFile() {
return "/help/tasks/junit/report.html";
}
@Override
public Publisher newInstance(StaplerRequest req, JSONObject formData)
throws hudson.model.Descriptor.FormException {
String testResults = formData.getString("testResults");
boolean keepLongStdio = formData.getBoolean("keepLongStdio");
DescribableList<TestDataPublisher, Descriptor<TestDataPublisher>> testDataPublishers = new DescribableList<TestDataPublisher, Descriptor<TestDataPublisher>>(Saveable.NOOP);
try {
testDataPublishers.rebuild(req, formData, TestDataPublisher.all());
} catch (IOException e) {
throw new FormException(e,null);
}
return new JUnitResultArchiver(testResults, keepLongStdio, testDataPublishers);
}
/**
* Performs on-the-fly validation on the file mask wildcard.
*/
public FormValidation doCheckTestResults(
@AncestorInPath AbstractProject project,
@QueryParameter String value) throws IOException {
return FilePath.validateFileMask(project.getSomeWorkspace(), value);
}
public boolean isApplicable(Class<? extends AbstractProject> jobType) {
return true;
}
public FormValidation doCheckHealthScaleFactor(@QueryParameter double value) {
if (value < 1e-7) return FormValidation.warning("Test health reporting disabled");
return FormValidation.ok(Messages.JUnitResultArchiver_HealthScaleFactorAnalysis(
1,
(int) (100.0 - Math.max(0.0, Math.min(100.0, 1 * value))),
5,
(int) (100.0 - Math.max(0.0, Math.min(100.0, 5 * value)))
));
}
}
}
/*
* The MIT License
*
* Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, Daniel Dyer, id:cactusman, Tom Huybrechts, Yahoo!, Inc.
*
* 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 hudson.tasks.junit;
import hudson.model.AbstractBuild;
import hudson.tasks.test.MetaTabulatedResult;
import hudson.tasks.test.TestResult;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import org.kohsuke.stapler.export.Exported;
import java.util.*;
/**
* Cumulative test result for a package.
*
* @author Kohsuke Kawaguchi
*/
public final class PackageResult extends MetaTabulatedResult implements Comparable<PackageResult> {
private final String packageName;
private transient String safeName;
/**
* All {@link ClassResult}s keyed by their short name.
*/
private final Map<String,ClassResult> classes = new TreeMap<String,ClassResult>();
private int passCount,failCount,skipCount;
private final hudson.tasks.junit.TestResult parent;
private float duration;
PackageResult(hudson.tasks.junit.TestResult parent, String packageName) {
this.packageName = packageName;
this.parent = parent;
}
@Override
public AbstractBuild<?, ?> getOwner() {
return (parent == null ? null : parent.getOwner());
}
public hudson.tasks.junit.TestResult getParent() {
return parent;
}
@Exported(visibility=999)
public String getName() {
return packageName;
}
@Override
public synchronized String getSafeName() {
if (safeName != null) {
return safeName;
}
Collection<PackageResult> siblings = (parent == null ? Collections.EMPTY_LIST : parent.getChildren());
return safeName = uniquifyName(
siblings,
safe(getName()));
}
@Override
public TestResult findCorrespondingResult(String id) {
String myID = safe(getName());
int base = id.indexOf(myID);
String className = id; // fall back value
if (base > 0) {
int classNameStart = base + myID.length() + 1;
if (classNameStart<id.length())
className = id.substring(classNameStart);
}
String subId = null;
int classNameEnd = className.indexOf('/');
if (classNameEnd > 0) {
subId = className.substring(classNameEnd + 1);
if (subId.length() == 0) {
subId = null;
}
className = className.substring(0, classNameEnd);
}
ClassResult child = getClassResult(className);
if (child != null && subId != null)
return child.findCorrespondingResult(subId);
return child;
}
@Override
public String getTitle() {
return Messages.PackageResult_getTitle(getDisplayName());
}
@Override
public String getChildTitle() {
return Messages.PackageResult_getChildTitle();
}
// TODO: wait until stapler 1.60 to do this @Exported
@Override
public float getDuration() {
return duration;
}
@Exported
@Override
public int getPassCount() {
return passCount;
}
@Exported
@Override
public int getFailCount() {
return failCount;
}
@Exported
@Override
public int getSkipCount() {
return skipCount;
}
@Override
public Object getDynamic(String name, StaplerRequest req, StaplerResponse rsp) {
ClassResult result = getClassResult(name);
if (result != null) {
return result;
} else {
return super.getDynamic(name, req, rsp);
}
}
public ClassResult getClassResult(String name) {
return classes.get(name);
}
@Exported(name="child")
public Collection<ClassResult> getChildren() {
return classes.values();
}
/**
* Whether this test result has children.
*/
@Override
public boolean hasChildren() {
int totalTests = passCount + failCount + skipCount;
return (totalTests != 0);
}
/**
* Returns a list of the failed cases, in no particular
* sort order
*/
public List<CaseResult> getFailedTests() {
List<CaseResult> r = new ArrayList<CaseResult>();
for (ClassResult clr : classes.values()) {
for (CaseResult cr : clr.getChildren()) {
if (cr.isFailed()) {
r.add(cr);
}
}
}
return r;
}
/**
* Returns a list of the failed cases, sorted by age.
*/
public List<CaseResult> getFailedTestsSortedByAge() {
List<CaseResult> failedTests = getFailedTests();
Collections.sort(failedTests, CaseResult.BY_AGE);
return failedTests;
}
/**
* Gets the "children" of this test result that passed
*
* @return the children of this test result, if any, or an empty collection
*/
@Override
public Collection<? extends hudson.tasks.test.TestResult> getPassedTests() {
List<CaseResult> r = new ArrayList<CaseResult>();
for (ClassResult clr : classes.values()) {
for (CaseResult cr : clr.getChildren()) {
if (cr.isPassed()) {
r.add(cr);
}
}
}
Collections.sort(r,CaseResult.BY_AGE);
return r;
}
/**
* Gets the "children" of this test result that were skipped
*
* @return the children of this test result, if any, or an empty list
*/
@Override
public Collection<? extends TestResult> getSkippedTests() {
List<CaseResult> r = new ArrayList<CaseResult>();
for (ClassResult clr : classes.values()) {
for (CaseResult cr : clr.getChildren()) {
if (cr.isSkipped()) {
r.add(cr);
}
}
}
Collections.sort(r, CaseResult.BY_AGE);
return r;
}
// /**
// * If this test failed, then return the build number
// * when this test started failing.
// */
// @Override
// TODO: implement! public int getFailedSince() {
// return 0; // (FIXME: generated)
// }
// /**
// * If this test failed, then return the run
// * when this test started failing.
// */
// TODO: implement! @Override
// public Run<?, ?> getFailedSinceRun() {
// return null; // (FIXME: generated)
// }
/**
* @return true if every test was not skipped and every test did not fail, false otherwise.
*/
@Override
public boolean isPassed() {
return (failCount == 0 && skipCount == 0);
}
void add(CaseResult r) {
String n = r.getSimpleName(), sn = safe(n);
ClassResult c = getClassResult(sn);
if (c == null) {
classes.put(sn,c=new ClassResult(this,n));
}
c.add(r);
duration += r.getDuration();
}
/**
* Recount my children
*/
@Override
public void tally() {
passCount = 0;
failCount = 0;
skipCount = 0;
duration = 0;
for (ClassResult cr : classes.values()) {
cr.tally();
passCount += cr.getPassCount();
failCount += cr.getFailCount();
skipCount += cr.getSkipCount();
duration += cr.getDuration();
}
}
void freeze() {
passCount = failCount = skipCount = 0;
for (ClassResult cr : classes.values()) {
cr.freeze();
passCount += cr.getPassCount();
failCount += cr.getFailCount();
skipCount += cr.getSkipCount();
}
}
public int compareTo(PackageResult that) {
return this.packageName.compareTo(that.packageName);
}
public String getDisplayName() {
return TestNameTransformer.getTransformedName(packageName);
}
}
/*
* The MIT License
*
* Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, Erik Ramfelt, Xavier Le Vourch, Tom Huybrechts, Yahoo!, Inc., Victor Garcia
*
* 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 hudson.tasks.junit;
import hudson.tasks.test.TestObject;
import hudson.util.io.ParserConfigurator;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import org.kohsuke.stapler.export.Exported;
import org.kohsuke.stapler.export.ExportedBean;
import java.io.File;
import java.io.IOException;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Result of one test suite.
*
* <p>
* The notion of "test suite" is rather arbitrary in JUnit ant task.
* It's basically one invocation of junit.
*
* <p>
* This object is really only used as a part of the persisted
* object tree.
*
* @author Kohsuke Kawaguchi
*/
@ExportedBean
public final class SuiteResult implements Serializable {
private final String file;
private final String name;
private final String stdout;
private final String stderr;
private float duration;
/**
* The 'timestamp' attribute of the test suite.
* AFAICT, this is not a required attribute in XML, so the value may be null.
*/
private String timestamp;
/** Optional ID attribute of a test suite. E.g., Eclipse plug-ins tests always have the name 'tests' but a different id. **/
private String id;
/**
* All test cases.
*/
private final List<CaseResult> cases = new ArrayList<CaseResult>();
private transient Map<String,CaseResult> casesByName;
private transient hudson.tasks.junit.TestResult parent;
SuiteResult(String name, String stdout, String stderr) {
this.name = name;
this.stderr = stderr;
this.stdout = stdout;
this.file = null;
}
private synchronized Map<String,CaseResult> casesByName() {
if (casesByName == null) {
casesByName = new HashMap<String,CaseResult>();
for (CaseResult c : cases) {
casesByName.put(c.getName(), c);
}
}
return casesByName;
}
/**
* Passed to {@link ParserConfigurator}.
* @since 1.416
*/
public static class SuiteResultParserConfigurationContext {
public final File xmlReport;
SuiteResultParserConfigurationContext(File xmlReport) {
this.xmlReport = xmlReport;
}
}
/**
* Parses the JUnit XML file into {@link SuiteResult}s.
* This method returns a collection, as a single XML may have multiple &lt;testsuite>
* elements wrapped into the top-level &lt;testsuites>.
*/
static List<SuiteResult> parse(File xmlReport, boolean keepLongStdio) throws DocumentException, IOException, InterruptedException {
List<SuiteResult> r = new ArrayList<SuiteResult>();
// parse into DOM
SAXReader saxReader = new SAXReader();
ParserConfigurator.applyConfiguration(saxReader,new SuiteResultParserConfigurationContext(xmlReport));
Document result = saxReader.read(xmlReport);
Element root = result.getRootElement();
parseSuite(xmlReport,keepLongStdio,r,root);
return r;
}
private static void parseSuite(File xmlReport, boolean keepLongStdio, List<SuiteResult> r, Element root) throws DocumentException, IOException {
// nested test suites
@SuppressWarnings("unchecked")
List<Element> testSuites = (List<Element>)root.elements("testsuite");
for (Element suite : testSuites)
parseSuite(xmlReport, keepLongStdio, r, suite);
// child test cases
// FIXME: do this also if no testcases!
if (root.element("testcase")!=null || root.element("error")!=null)
r.add(new SuiteResult(xmlReport, root, keepLongStdio));
}
/**
* @param xmlReport
* A JUnit XML report file whose top level element is 'testsuite'.
* @param suite
* The parsed result of {@code xmlReport}
*/
private SuiteResult(File xmlReport, Element suite, boolean keepLongStdio) throws DocumentException, IOException {
this.file = xmlReport.getAbsolutePath();
String name = suite.attributeValue("name");
if(name==null)
// some user reported that name is null in their environment.
// see http://www.nabble.com/Unexpected-Null-Pointer-Exception-in-Hudson-1.131-tf4314802.html
name = '('+xmlReport.getName()+')';
else {
String pkg = suite.attributeValue("package");
if(pkg!=null&& pkg.length()>0) name=pkg+'.'+name;
}
this.name = TestObject.safe(name);
this.timestamp = suite.attributeValue("timestamp");
this.id = suite.attributeValue("id");
Element ex = suite.element("error");
if(ex!=null) {
// according to junit-noframes.xsl l.229, this happens when the test class failed to load
addCase(new CaseResult(this, suite, "<init>", keepLongStdio));
}
@SuppressWarnings("unchecked")
List<Element> testCases = (List<Element>)suite.elements("testcase");
for (Element e : testCases) {
// https://issues.jenkins-ci.org/browse/JENKINS-1233 indicates that
// when <testsuites> is present, we are better off using @classname on the
// individual testcase class.
// https://issues.jenkins-ci.org/browse/JENKINS-1463 indicates that
// @classname may not exist in individual testcase elements. We now
// also test if the testsuite element has a package name that can be used
// as the class name instead of the file name which is default.
String classname = e.attributeValue("classname");
if (classname == null) {
classname = suite.attributeValue("name");
}
// https://issues.jenkins-ci.org/browse/JENKINS-1233 and
// http://www.nabble.com/difference-in-junit-publisher-and-ant-junitreport-tf4308604.html#a12265700
// are at odds with each other --- when both are present,
// one wants to use @name from <testsuite>,
// the other wants to use @classname from <testcase>.
addCase(new CaseResult(this, e, classname, keepLongStdio));
}
String stdout = CaseResult.possiblyTrimStdio(cases, keepLongStdio, suite.elementText("system-out"));
String stderr = CaseResult.possiblyTrimStdio(cases, keepLongStdio, suite.elementText("system-err"));
if (stdout==null && stderr==null) {
// Surefire never puts stdout/stderr in the XML. Instead, it goes to a separate file (when ${maven.test.redirectTestOutputToFile}).
Matcher m = SUREFIRE_FILENAME.matcher(xmlReport.getName());
if (m.matches()) {
// look for ***-output.txt from TEST-***.xml
File mavenOutputFile = new File(xmlReport.getParentFile(),m.group(1)+"-output.txt");
if (mavenOutputFile.exists()) {
try {
stdout = CaseResult.possiblyTrimStdio(cases, keepLongStdio, mavenOutputFile);
} catch (IOException e) {
throw new IOException("Failed to read "+mavenOutputFile,e);
}
}
}
}
this.stdout = stdout;
this.stderr = stderr;
}
/*package*/ void addCase(CaseResult cr) {
cases.add(cr);
casesByName().put(cr.getName(), cr);
duration += cr.getDuration();
}
@Exported(visibility=9)
public String getName() {
return name;
}
@Exported(visibility=9)
public float getDuration() {
return duration;
}
/**
* The stdout of this test.
*
* @since 1.281
* @see CaseResult#getStdout()
*/
@Exported
public String getStdout() {
return stdout;
}
/**
* The stderr of this test.
*
* @since 1.281
* @see CaseResult#getStderr()
*/
@Exported
public String getStderr() {
return stderr;
}
/**
* The absolute path to the original test report. OS-dependent.
*/
public String getFile() {
return file;
}
public hudson.tasks.junit.TestResult getParent() {
return parent;
}
@Exported(visibility=9)
public String getTimestamp() {
return timestamp;
}
@Exported(visibility=9)
public String getId() {
return id;
}
@Exported(inline=true,visibility=9)
public List<CaseResult> getCases() {
return cases;
}
public SuiteResult getPreviousResult() {
hudson.tasks.test.TestResult pr = parent.getPreviousResult();
if(pr==null) return null;
if(pr instanceof hudson.tasks.junit.TestResult)
return ((hudson.tasks.junit.TestResult)pr).getSuite(name);
return null;
}
/**
* Returns the {@link CaseResult} whose {@link CaseResult#getName()}
* is the same as the given string.
*
* <p>
* Note that test name needs not be unique.
*/
public CaseResult getCase(String name) {
return casesByName().get(name);
}
public Set<String> getClassNames() {
Set<String> result = new HashSet<String>();
for (CaseResult c : cases) {
result.add(c.getClassName());
}
return result;
}
/** KLUGE. We have to call this to prevent freeze()
* from calling c.freeze() on all its children,
* because that in turn calls c.getOwner(),
* which requires a non-null parent.
* @param parent
*/
void setParent(hudson.tasks.junit.TestResult parent) {
this.parent = parent;
}
/*package*/ boolean freeze(hudson.tasks.junit.TestResult owner) {
if(this.parent!=null)
return false; // already frozen
this.parent = owner;
for (CaseResult c : cases)
c.freeze(this);
return true;
}
private static final long serialVersionUID = 1L;
private static final Pattern SUREFIRE_FILENAME = Pattern.compile("TEST-(.+)\\.xml");
}
/*
* The MIT License
*
* Copyright (c) 2004-2009, Sun Microsystems, Inc., Tom Huybrechts
*
* 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 hudson.tasks.junit;
import hudson.model.Action;
/**
*
* Jelly (all optional):
* <ul>
* <li>index.jelly: included at the top of the test page</li>
* <li>summary.jelly: included in a collapsed panel on the test parent page</li>
* <li>badge.jelly: shown after the test link on the test parent page</li>
* </ul>
*
* @author tom
* @since 1.320
* @see TestDataPublisher
*/
public abstract class TestAction implements Action {
/**
* Returns text with annotations.
*/
public String annotate(String text) {
return text;
}
}
/*
* The MIT License
*
* Copyright (c) 2004-2009, Sun Microsystems, Inc., Tom Huybrechts, Yahoo!, Inc.
*
* 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 hudson.tasks.junit;
import hudson.DescriptorExtensionList;
import hudson.Extension;
import hudson.ExtensionPoint;
import hudson.Launcher;
import hudson.model.*;
import jenkins.model.Jenkins;
import java.io.IOException;
/**
* Contributes {@link TestAction}s to test results.
*
* This enables plugins to annotate test results and provide richer UI, such as letting users
* claim test failures, allowing people to file bugs, or more generally, additional actions, views, etc.
*
* <p>
* To register your implementation, put {@link Extension} on your descriptor implementation.
*
* @since 1.320
*/
public abstract class TestDataPublisher extends AbstractDescribableImpl<TestDataPublisher> implements ExtensionPoint {
/**
* Called after test results are collected by Jenkins, to create a resolver for {@link TestAction}s.
*
* @return
* can be null to indicate that there's nothing to contribute for this test result.
*/
public abstract TestResultAction.Data getTestData(
AbstractBuild<?, ?> build, Launcher launcher,
BuildListener listener, TestResult testResult) throws IOException, InterruptedException;
public static DescriptorExtensionList<TestDataPublisher, Descriptor<TestDataPublisher>> all() {
return Jenkins.getInstance().<TestDataPublisher, Descriptor<TestDataPublisher>>getDescriptorList(TestDataPublisher.class);
}
}
package hudson.tasks.junit;
import hudson.ExtensionList;
import hudson.ExtensionPoint;
import jenkins.model.Jenkins;
/**
* Allow extensions to transform the class/package/method name for JUnit test
* cases which will be displayed on the test result page.
*
* This is useful for alternative JVM languages like Scala that allow
* identifiers with invalid characters by encoding them: an extension can
* decode the identifier so it is displayed correctly.
*
* @since 1.515
*/
public abstract class TestNameTransformer implements ExtensionPoint {
/**
* Transform the class/package/method name.
*
* @param name
* Class name (may be simple or fully qualified), package name, or
* method name from a JUnit test.
* @return
* The transformed name, or the name that was passed in if it doesn't
* need to be changed.
*/
public abstract String transformName(String name);
public static String getTransformedName(String name) {
String transformedName = name;
for (TestNameTransformer transformer : all()) {
transformedName = transformer.transformName(transformedName);
}
return transformedName;
}
public static ExtensionList<TestNameTransformer> all() {
return ExtensionList.lookup(TestNameTransformer.class);
}
}
/*
* The MIT License
*
* Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, Tom Huybrechts, Yahoo! Inc., InfraDNA, Inc.
*
* 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 hudson.tasks.junit;
import hudson.model.AbstractBuild;
import hudson.model.AbstractModelObject;
import hudson.model.Api;
import hudson.tasks.test.AbstractTestResultAction;
import org.kohsuke.stapler.export.ExportedBean;
import java.io.Serializable;
import java.util.List;
/**
* Stub of base class for all test result objects. The real implementation of
* the TestObject is in hudson.tasks.test.TestObject. This class simply
* defines abstract methods so that legacy code will continue to compile.
*
* @deprecated
* Use {@link hudson.tasks.test.TestObject} instead.
*
* @author Kohsuke Kawaguchi
*/
@ExportedBean
public abstract class TestObject extends AbstractModelObject implements Serializable {
public abstract AbstractBuild<?,?> getOwner() ;
public abstract TestObject getParent();
public abstract String getId();
/**
* Returns url relative to TestResult
*/
public abstract String getUrl();
public abstract TestResult getTestResult();
public abstract AbstractTestResultAction getTestResultAction();
public abstract List<TestAction> getTestActions();
public abstract <T> T getTestAction(Class<T> klazz);
/**
* Gets the counter part of this {@link TestObject} in the previous run.
*
* @return null if no such counter part exists.
*/
public abstract TestObject getPreviousResult();
public abstract TestObject getResultInBuild(AbstractBuild<?,?> build);
/**
* Time took to run this test. In seconds.
*/
public abstract float getDuration();
/**
* Returns the string representation of the {@link #getDuration()}, in a
* human readable format.
*/
public abstract String getDurationString();
public abstract String getDescription();
public abstract void setDescription(String description);
/**
* Exposes this object through the remote API.
*/
public abstract Api getApi();
/**
* Gets the name of this object.
*/
public abstract String getName();
/**
* Gets the version of {@link #getName()} that's URL-safe.
*/
public abstract String getSafeName();
public abstract String getSearchUrl();
/**
* Gets the total number of passed tests.
*/
public abstract int getPassCount();
/**
* Gets the total number of failed tests.
*/
public abstract int getFailCount();
/**
* Gets the total number of skipped tests.
*/
public abstract int getSkipCount();
/**
* Gets the total number of tests.
*/
public abstract int getTotalCount();
public abstract History getHistory();
// public abstract Object getDynamic(String token, StaplerRequest req,
// StaplerResponse rsp);
//
// public abstract HttpResponse doSubmitDescription(
// @QueryParameter String description) throws IOException,
// ServletException;
}
/*
* The MIT License
*
* Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, Daniel Dyer, Red Hat, Inc., Tom Huybrechts, Yahoo!, Inc.
*
* 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 hudson.tasks.junit;
import com.thoughtworks.xstream.XStream;
import hudson.XmlFile;
import hudson.model.AbstractBuild;
import hudson.model.AbstractProject;
import hudson.model.Action;
import hudson.model.BuildListener;
import hudson.tasks.test.AbstractTestResultAction;
import hudson.tasks.test.TestObject;
import hudson.util.HeapSpaceStringConverter;
import hudson.util.XStream2;
import org.kohsuke.stapler.StaplerProxy;
import java.io.File;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* {@link Action} that displays the JUnit test result.
*
* <p>
* The actual test reports are isolated by {@link WeakReference}
* so that it doesn't eat up too much memory.
*
* @author Kohsuke Kawaguchi
*/
public class TestResultAction extends AbstractTestResultAction<TestResultAction> implements StaplerProxy {
private transient WeakReference<TestResult> result;
// Hudson < 1.25 didn't set these fields, so use Integer
// so that we can distinguish between 0 tests vs not-computed-yet.
private int failCount;
private int skipCount;
private Integer totalCount;
private Double healthScaleFactor;
private List<Data> testData = new ArrayList<Data>();
@Deprecated
public TestResultAction(AbstractBuild owner, TestResult result, BuildListener listener) {
super(owner);
setResult(result, listener);
}
/** @since 1.545 */
public TestResultAction(TestResult result, BuildListener listener) {
this(null, result, listener);
}
/**
* Overwrites the {@link TestResult} by a new data set.
*/
public synchronized void setResult(TestResult result, BuildListener listener) {
result.freeze(this);
totalCount = result.getTotalCount();
failCount = result.getFailCount();
skipCount = result.getSkipCount();
// persist the data
try {
getDataFile().write(result);
} catch (IOException e) {
e.printStackTrace(listener.fatalError("Failed to save the JUnit test result"));
}
this.result = new WeakReference<TestResult>(result);
}
private XmlFile getDataFile() {
return new XmlFile(XSTREAM,new File(owner.getRootDir(), "junitResult.xml"));
}
public synchronized TestResult getResult() {
TestResult r;
if(result==null) {
r = load();
result = new WeakReference<TestResult>(r);
} else {
r = result.get();
}
if(r==null) {
r = load();
result = new WeakReference<TestResult>(r);
}
if(totalCount==null) {
totalCount = r.getTotalCount();
failCount = r.getFailCount();
skipCount = r.getSkipCount();
}
return r;
}
@Override
public int getFailCount() {
if(totalCount==null)
getResult(); // this will compute the result
return failCount;
}
@Override
public int getSkipCount() {
if(totalCount==null)
getResult(); // this will compute the result
return skipCount;
}
@Override
public int getTotalCount() {
if(totalCount==null)
getResult(); // this will compute the result
return totalCount;
}
@Override
public double getHealthScaleFactor() {
return healthScaleFactor == null ? 1.0 : healthScaleFactor;
}
public void setHealthScaleFactor(double healthScaleFactor) {
this.healthScaleFactor = Math.max(0.0,healthScaleFactor);
}
@Override
public List<CaseResult> getFailedTests() {
return getResult().getFailedTests();
}
/**
* Loads a {@link TestResult} from disk.
*/
private TestResult load() {
TestResult r;
try {
r = (TestResult)getDataFile().read();
} catch (IOException e) {
logger.log(Level.WARNING, "Failed to load "+getDataFile(),e);
r = new TestResult(); // return a dummy
}
r.freeze(this);
return r;
}
public Object getTarget() {
return getResult();
}
public List<TestAction> getActions(TestObject object) {
List<TestAction> result = new ArrayList<TestAction>();
// Added check for null testData to avoid NPE from issue 4257.
if (testData != null) {
for (Data data : testData)
for (TestAction ta : data.getTestAction(object))
if (ta != null)
result.add(ta);
}
return Collections.unmodifiableList(result);
}
public void setData(List<Data> testData) {
this.testData = testData;
}
/**
* Resolves {@link TestAction}s for the given {@link TestObject}.
*
* <p>
* This object itself is persisted as a part of {@link AbstractBuild}, so it needs to be XStream-serializable.
*
* @see TestDataPublisher
*/
public static abstract class Data {
/**
* Returns all TestActions for the testObject.
*
* @return
* Can be empty but never null. The caller must assume that the returned list is read-only.
*/
public abstract List<? extends TestAction> getTestAction(hudson.tasks.junit.TestObject testObject);
}
public Object readResolve() {
super.readResolve(); // let it do the post-deserialization work
if (testData == null) {
testData = new ArrayList<Data>(0);
}
return this;
}
private static final Logger logger = Logger.getLogger(TestResultAction.class.getName());
private static final XStream XSTREAM = new XStream2();
static {
XSTREAM.alias("result",TestResult.class);
XSTREAM.alias("suite",SuiteResult.class);
XSTREAM.alias("case",CaseResult.class);
XSTREAM.registerConverter(new HeapSpaceStringConverter(),100);
}
}
/*
* The MIT License
*
* Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, Jorg Heymans
*
* 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 hudson.tasks.junit;
import hudson.Extension;
import hudson.tasks.junit.SuiteResult.SuiteResultParserConfigurationContext;
import hudson.util.io.ParserConfigurator;
import org.dom4j.io.SAXReader;
import org.xml.sax.EntityResolver;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import java.io.IOException;
import java.net.URL;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* As the name suggest: a resolver for XML entities.
*
* <p>
* Basically, it provides the possibility to intercept online DTD lookups
* and instead do offline lookup by redirecting to a local directory where
* .dtd's are stored
*
* (useful when parsing testng-results.xml - which points to testng.org)
*
* @author Mikael Carneholm
*/
@Extension
public class XMLEntityResolver extends ParserConfigurator implements EntityResolver {
private static final String TESTNG_NAMESPACE = "http://testng.org/";
/**
* Intercepts the lookup of publicId, systemId
*/
public InputSource resolveEntity(String publicId, String systemId) throws SAXException, IOException {
if (systemId != null) {
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("Will try to resolve systemId [" + systemId + "]");
}
// TestNG system-ids
if (systemId.startsWith(TESTNG_NAMESPACE)) {
LOGGER.fine("It's a TestNG document, will try to lookup DTD in classpath");
String dtdFileName = systemId.substring(TESTNG_NAMESPACE.length());
URL url = getClass().getClassLoader().getResource(dtdFileName);
if (url != null)
return new InputSource(url.toString());
}
}
// Default fallback
return null;
}
/**
* Install EntityResolver for resolving DTDs, which are in files created by TestNG.
*/
@Override
public void configure(SAXReader reader, Object context) {
if (context instanceof SuiteResultParserConfigurationContext) {
reader.setEntityResolver(this);
}
}
private static final Logger LOGGER = Logger.getLogger(XMLEntityResolver.class.getName());
}
<!--
The MIT License
Copyright (c) 2004-2010, Sun Microsystems, Inc., Kohsuke Kawaguchi
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.
-->
<html><head/><body>
Model objects that represent JUnit test reports.
</body></html>
\ No newline at end of file
/*
* The MIT License
*
* Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, Daniel Dyer, Red Hat, Inc., Stephen Connolly, id:cactusman, Yahoo!, Inc.
*
* 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 hudson.tasks.test;
import hudson.Extension;
import hudson.Functions;
import hudson.model.*;
import hudson.util.*;
import hudson.util.ChartUtil.NumberOnlyBuildLabel;
import java.awt.*;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import jenkins.model.RunAction2;
import org.jfree.chart.ChartFactory;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.axis.CategoryAxis;
import org.jfree.chart.axis.CategoryLabelPositions;
import org.jfree.chart.axis.NumberAxis;
import org.jfree.chart.plot.CategoryPlot;
import org.jfree.chart.plot.PlotOrientation;
import org.jfree.chart.renderer.category.StackedAreaRenderer;
import org.jfree.data.category.CategoryDataset;
import org.jfree.ui.RectangleInsets;
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;
/**
* 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, RunAction2 {
public transient AbstractBuild<?,?> owner;
private Map<String,String> descriptions = new ConcurrentHashMap<String, String>();
/** @since 1.545 */
protected AbstractTestResultAction() {}
/** @deprecated Use the default constructor and just call {@link Run#addAction} to associate the build with the action. */
@Deprecated
protected AbstractTestResultAction(AbstractBuild owner) {
this.owner = owner;
}
@Override public void onAttached(Run<?, ?> r) {
this.owner = (AbstractBuild<?,?>) r;
}
@Override public void onLoad(Run<?, ?> r) {
this.owner = (AbstractBuild<?,?>) r;
}
/**
* 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 HealthReport getBuildHealth() {
final double scaleFactor = getHealthScaleFactor();
if (scaleFactor < 1e-7) {
return null;
}
final int totalCount = getTotalCount();
final int failCount = getFailCount();
int score = (totalCount == 0)
? 100
: (int) (100.0 * Math.max(1.0, Math.min(0.0, 1.0 - (scaleFactor * 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);
}
/**
* Returns how much to scale the test related health by.
* @return a factor of {@code 1.0} to have the test health be the percentage of tests passing so 20% of tests
* failing will report as 80% health. A factor of {@code 2.0} will mean that 20% of tests failing will report as 60%
* health. A factor of {@code 2.5} will mean that 20% of test failing will report as 50% health. A factor of
* {@code 4.0} will mean that 20% of tests failing will report as 20% health. A factor of {@code 5.0} will mean
* that 20% (or more) of tests failing will report as 0% health. A factor of {@code 0.0} will disable test health
* reporting.
*/
public double getHealthScaleFactor() {
return 1.0;
}
/**
* 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(), true);
}
private <U extends AbstractTestResultAction> U getPreviousResult(Class<U> type, boolean eager) {
Set<Integer> loadedBuilds = eager ? null : owner.getProject()._getRuns().getLoadedBuilds().keySet();
AbstractBuild<?,?> b = owner;
while(true) {
b = eager || loadedBuilds.contains(b.number - /* assuming there are no gaps */1) ? b.getPreviousBuild() : null;
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<? extends TestResult> getFailedTests() {
return Collections.emptyList();
}
/**
* 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;
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;
ChartUtil.generateClickableMap(req,rsp,createChart(req,buildDataSet(req)),calcDefaultSize());
}
/**
* Returns a full path down to a test result
*/
public String getTestResultPath(TestResult it) {
return getUrlName() + "/" + it.getRelativePathFrom(null);
}
/**
* 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 CategoryDataset buildDataSet(StaplerRequest req) {
boolean failureOnly = Boolean.valueOf(req.getParameter("failureOnly"));
DataSetBuilder<String,NumberOnlyBuildLabel> dsb = new DataSetBuilder<String,NumberOnlyBuildLabel>();
for (AbstractTestResultAction<?> a = this; a != null; a = a.getPreviousResult(AbstractTestResultAction.class, false)) {
dsb.add( a.getFailCount(), "failed", new NumberOnlyBuildLabel(a.owner));
if(!failureOnly) {
dsb.add( a.getSkipCount(), "skipped", new NumberOnlyBuildLabel(a.owner));
dsb.add( a.getTotalCount()-a.getFailCount()-a.getSkipCount(),"total", new NumberOnlyBuildLabel(a.owner));
}
}
return dsb.build();
}
private JFreeChart createChart(StaplerRequest req,CategoryDataset dataset) {
final String relPath = getRelPath(req);
final JFreeChart chart = ChartFactory.createStackedAreaChart(
null, // chart title
null, // unused
"count", // range axis label
dataset, // data
PlotOrientation.VERTICAL, // orientation
false, // include legend
true, // tooltips
false // urls
);
// NOW DO SOME OPTIONAL CUSTOMISATION OF THE CHART...
// set the background color for the chart...
// final StandardLegend legend = (StandardLegend) chart.getLegend();
// legend.setAnchor(StandardLegend.SOUTH);
chart.setBackgroundPaint(Color.white);
final CategoryPlot plot = chart.getCategoryPlot();
// plot.setAxisOffset(new Spacer(Spacer.ABSOLUTE, 5.0, 5.0, 5.0, 5.0));
plot.setBackgroundPaint(Color.WHITE);
plot.setOutlinePaint(null);
plot.setForegroundAlpha(0.8f);
// plot.setDomainGridlinesVisible(true);
// plot.setDomainGridlinePaint(Color.white);
plot.setRangeGridlinesVisible(true);
plot.setRangeGridlinePaint(Color.black);
CategoryAxis domainAxis = new ShiftedCategoryAxis(null);
plot.setDomainAxis(domainAxis);
domainAxis.setCategoryLabelPositions(CategoryLabelPositions.UP_90);
domainAxis.setLowerMargin(0.0);
domainAxis.setUpperMargin(0.0);
domainAxis.setCategoryMargin(0.0);
final NumberAxis rangeAxis = (NumberAxis) plot.getRangeAxis();
rangeAxis.setStandardTickUnits(NumberAxis.createIntegerTickUnits());
StackedAreaRenderer ar = new StackedAreaRenderer2() {
@Override
public String generateURL(CategoryDataset dataset, int row, int column) {
NumberOnlyBuildLabel label = (NumberOnlyBuildLabel) dataset.getColumnKey(column);
return relPath+label.build.getNumber()+"/testReport/";
}
@Override
public String generateToolTip(CategoryDataset dataset, int row, int column) {
NumberOnlyBuildLabel label = (NumberOnlyBuildLabel) dataset.getColumnKey(column);
AbstractTestResultAction a = label.build.getAction(AbstractTestResultAction.class);
switch (row) {
case 0:
return String.valueOf(Messages.AbstractTestResultAction_fail(label.build.getDisplayName(), a.getFailCount()));
case 1:
return String.valueOf(Messages.AbstractTestResultAction_skip(label.build.getDisplayName(), a.getSkipCount()));
default:
return String.valueOf(Messages.AbstractTestResultAction_test(label.build.getDisplayName(), a.getTotalCount()));
}
}
};
plot.setRenderer(ar);
ar.setSeriesPaint(0,ColorPalette.RED); // Failures.
ar.setSeriesPaint(1,ColorPalette.YELLOW); // Skips.
ar.setSeriesPaint(2,ColorPalette.BLUE); // Total.
// crop extra space around the graph
plot.setInsets(new RectangleInsets(0,0,0,5.0));
return chart;
}
private String getRelPath(StaplerRequest req) {
String relPath = req.getParameter("rel");
if(relPath==null) return "";
return relPath;
}
/**
* {@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;
}
@Extension public static final class Summarizer extends Run.StatusSummarizer {
@Override public Run.Summary summarize(Run<?,?> run, ResultTrend trend) {
AbstractTestResultAction<?> trN = run.getAction(AbstractTestResultAction.class);
if (trN == null) {
return null;
}
Boolean worseOverride;
switch (trend) {
case NOW_UNSTABLE:
worseOverride = false;
break;
case UNSTABLE:
worseOverride = true;
break;
case STILL_UNSTABLE:
worseOverride = null;
break;
default:
return null;
}
Run prev = run.getPreviousBuild();
AbstractTestResultAction<?> trP = prev == null ? null : prev.getAction(AbstractTestResultAction.class);
if (trP == null) {
if (trN.getFailCount() > 0) {
return new Run.Summary(worseOverride != null ? worseOverride : true, Messages.Run_Summary_TestFailures(trN.getFailCount()));
}
} else {
if (trN.getFailCount() != 0) {
if (trP.getFailCount() == 0) {
return new Run.Summary(worseOverride != null ? worseOverride : true, Messages.Run_Summary_TestsStartedToFail(trN.getFailCount()));
}
if (trP.getFailCount() < trN.getFailCount()) {
return new Run.Summary(worseOverride != null ? worseOverride : true, Messages.Run_Summary_MoreTestsFailing(trN.getFailCount() - trP.getFailCount(), trN.getFailCount()));
}
if (trP.getFailCount() > trN.getFailCount()) {
return new Run.Summary(worseOverride != null ? worseOverride : false, Messages.Run_Summary_LessTestsFailing(trP.getFailCount() - trN.getFailCount(), trN.getFailCount()));
}
return new Run.Summary(worseOverride != null ? worseOverride : false, Messages.Run_Summary_TestsStillFailing(trN.getFailCount()));
}
}
return null;
}
}
}
/*
* The MIT License
*
* Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, Daniel Dyer, Red Hat, Inc., Yahoo!, Inc.
*
* 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 hudson.tasks.test;
import hudson.model.AbstractBuild;
import org.kohsuke.stapler.export.Exported;
import org.kohsuke.stapler.export.ExportedBean;
import java.util.AbstractList;
import java.util.ArrayList;
import java.util.List;
/**
* {@link AbstractTestResultAction} that aggregates all the test results
* from the corresponding {@link AbstractBuild}s.
*
* <p>
* (This has nothing to do with {@link AggregatedTestResultPublisher}, unfortunately)
*
* @author Kohsuke Kawaguchi
*/
@ExportedBean
public abstract class AggregatedTestResultAction extends AbstractTestResultAction {
private int failCount,skipCount,totalCount;
public static final class Child {
/**
* Name of the module. Could be relative to something.
* The interpretation of this is done by
* {@link AggregatedTestResultAction#getChildName(AbstractTestResultAction)} and
* {@link AggregatedTestResultAction#resolveChild(Child)} and
*/
public final String name;
public final int build;
public Child(String name, int build) {
this.name = name;
this.build = build;
}
}
/**
* child builds whose test results are used for aggregation.
*/
public final List<Child> children = new ArrayList<Child>();
@Deprecated
public AggregatedTestResultAction(AbstractBuild owner) {
super(owner);
}
/** @since 1.545 */
public AggregatedTestResultAction() {}
protected void update(List<? extends AbstractTestResultAction> children) {
failCount = skipCount = totalCount = 0;
this.children.clear();
for (AbstractTestResultAction tr : children)
add(tr);
}
protected void add(AbstractTestResultAction child) {
failCount += child.getFailCount();
skipCount += child.getSkipCount();
totalCount += child.getTotalCount();
this.children.add(new Child(getChildName(child),child.owner.number));
}
public int getFailCount() {
return failCount;
}
@Override
public int getSkipCount() {
return skipCount;
}
public int getTotalCount() {
return totalCount;
}
public List<ChildReport> getResult() {
// I think this is a reasonable default.
return getChildReports();
}
@Override
public List<? extends TestResult> getFailedTests() {
List<TestResult> failedTests = new ArrayList<TestResult>(failCount);
for (ChildReport childReport : getChildReports()) {
if (childReport.result instanceof TestResult) {
failedTests.addAll(((TestResult) childReport.result).getFailedTests());
}
}
return failedTests;
}
/**
* Data-binding bean for the remote API.
*/
@ExportedBean(defaultVisibility=2)
public static final class ChildReport {
@Exported
public final AbstractBuild<?,?> child;
@Exported
public final Object result;
public ChildReport(AbstractBuild<?, ?> child, AbstractTestResultAction result) {
this.child = child;
this.result = result!=null ? result.getResult() : null;
}
}
/**
* Mainly for the remote API. Expose results from children.
*/
@Exported(inline=true)
public List<ChildReport> getChildReports() {
return new AbstractList<ChildReport>() {
public ChildReport get(int index) {
return new ChildReport(
resolveChild(children.get(index)),
getChildReport(children.get(index)));
}
public int size() {
return children.size();
}
};
}
protected abstract String getChildName(AbstractTestResultAction tr);
public abstract AbstractBuild<?,?> resolveChild(Child child);
/**
* Uses {@link #resolveChild(Child)} and obtain the
* {@link AbstractTestResultAction} object for the given child.
*/
protected AbstractTestResultAction getChildReport(Child child) {
AbstractBuild<?,?> b = resolveChild(child);
if(b==null) return null;
return b.getAction(AbstractTestResultAction.class);
}
/**
* Since there's no TestObject that points this action as the owner
* (aggregated {@link TestObject}s point to their respective real owners, not 'this'),
* so this method should be never invoked.
*
* @deprecated
* so that IDE warns you if you accidentally try to call it.
*/
@Override
protected final String getDescription(TestObject object) {
throw new AssertionError();
}
/**
* See {@link #getDescription(TestObject)}
*
* @deprecated
* so that IDE warns you if you accidentally try to call it.
*/
@Override
protected final void setDescription(TestObject object, String description) {
throw new AssertionError();
}
}
/*
* The MIT License
*
* Copyright (c) 2004-2011, Sun Microsystems, Inc., Kohsuke Kawaguchi, Michael B. Donohue, Yahoo!, Inc., Andrew Bayer
*
* 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 hudson.tasks.test;
import hudson.model.AbstractBuild;
import hudson.model.AbstractProject;
import hudson.model.AutoCompletionCandidates;
import hudson.Extension;
import hudson.Launcher;
import hudson.Util;
import static hudson.Util.fixNull;
import hudson.model.Action;
import hudson.model.BuildListener;
import hudson.model.Fingerprint.RangeSet;
import hudson.model.InvisibleAction;
import hudson.model.ItemGroup;
import jenkins.model.Jenkins;
import hudson.model.Item;
import hudson.model.Job;
import hudson.model.Result;
import hudson.model.Run;
import hudson.model.TaskListener;
import hudson.model.listeners.RunListener;
import hudson.tasks.BuildStepDescriptor;
import hudson.tasks.BuildStepMonitor;
import hudson.tasks.Fingerprinter.FingerprintAction;
import hudson.tasks.Publisher;
import hudson.tasks.Recorder;
import hudson.util.FormValidation;
import net.sf.json.JSONObject;
import org.kohsuke.stapler.AncestorInPath;
import org.kohsuke.stapler.QueryParameter;
import org.kohsuke.stapler.StaplerRequest;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import javax.annotation.CheckForNull;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
/**
* Aggregates downstream test reports into a single consolidated report,
* so that people can see the overall test results in one page
* when tests are scattered across many different jobs.
*
* @author Kohsuke Kawaguchi
*/
public class AggregatedTestResultPublisher extends Recorder {
/**
* Jobs to aggregate. Comma separated.
* Null if triggering downstreams.
*/
public final String jobs;
/**
* Should failed builds be included?
*/
public final boolean includeFailedBuilds;
public AggregatedTestResultPublisher(String jobs) {
this(jobs, false);
}
public AggregatedTestResultPublisher(String jobs, boolean includeFailedBuilds) {
this.jobs = Util.fixEmptyAndTrim(jobs);
this.includeFailedBuilds = includeFailedBuilds;
}
public boolean perform(AbstractBuild<?,?> build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException {
// add a TestResult just so that it can show up later.
build.addAction(new TestResultAction(jobs, includeFailedBuilds, build));
return true;
}
public BuildStepMonitor getRequiredMonitorService() {
return BuildStepMonitor.NONE;
}
@Override public Collection<? extends Action> getProjectActions(AbstractProject<?, ?> project) {
return Collections.singleton(new TestResultProjectAction(project));
}
/**
* Action that serves the aggregated record.
*
* TODO: persist some information so that even when some of the individuals
* are gone, we can still retain some useful information.
*/
public static final class TestResultAction extends AbstractTestResultAction {
/**
* Jobs to aggregate. Comma separated.
* Null if doing downstream projects.
*/
private final @CheckForNull String jobs;
/**
* Should failed builds be included?
*/
private final boolean includeFailedBuilds;
/**
* The last time the fields of this object is computed from the rest.
*/
private transient long lastUpdated = 0;
/**
* When was the last time any build completed?
*/
private static long lastChanged = 0;
private transient int failCount;
private transient int totalCount;
private transient List<AbstractTestResultAction> individuals;
/**
* Projects that haven't run yet.
*/
private transient List<AbstractProject> didntRun;
private transient List<AbstractProject> noFingerprints;
@SuppressWarnings("deprecation") // calls getProject in constructor, so needs owner immediately
public TestResultAction(String jobs, boolean includeFailedBuilds, AbstractBuild<?,?> owner) {
super(owner);
this.includeFailedBuilds = includeFailedBuilds;
if(jobs==null) {
// resolve null as the transitive downstream jobs
StringBuilder buf = new StringBuilder();
for (AbstractProject p : getProject().getTransitiveDownstreamProjects()) {
if(buf.length()>0) buf.append(',');
buf.append(p.getFullName());
}
jobs = buf.toString();
}
this.jobs = jobs;
}
/**
* Gets the jobs to be monitored.
*/
public Collection<AbstractProject> getJobs() {
List<AbstractProject> r = new ArrayList<AbstractProject>();
for (String job : Util.tokenize(jobs,",")) {
AbstractProject j = Jenkins.getInstance().getItemByFullName(job.trim(), AbstractProject.class);
if(j!=null)
r.add(j);
}
return r;
}
public boolean getIncludeFailedBuilds() {
return includeFailedBuilds;
}
private AbstractProject<?,?> getProject() {
return owner.getProject();
}
public int getFailCount() {
upToDateCheck();
return failCount;
}
public int getTotalCount() {
upToDateCheck();
return totalCount;
}
public Object getResult() {
upToDateCheck();
return this;
}
/**
* Since there's no TestObject that points this action as the owner
* (aggregated {@link TestObject}s point to their respective real owners, not 'this'),
* so this method should be never invoked.
*
* @deprecated
* so that IDE warns you if you accidentally try to call it.
*/
@Override
protected String getDescription(TestObject object) {
throw new AssertionError();
}
/**
* See {@link #getDescription(TestObject)}
*
* @deprecated
* so that IDE warns you if you accidentally try to call it.
*/
@Override
protected void setDescription(TestObject object, String description) {
throw new AssertionError();
}
/**
* Returns the individual test results that are aggregated.
*/
public List<AbstractTestResultAction> getIndividuals() {
upToDateCheck();
return Collections.unmodifiableList(individuals);
}
/**
* Gets the downstream projects that haven't run yet, but
* expected to produce test results.
*/
public List<AbstractProject> getDidntRun() {
return Collections.unmodifiableList(didntRun);
}
/**
* Gets the downstream projects that have available test results, but
* do not appear to have fingerprinting enabled.
*/
public List<AbstractProject> getNoFingerprints() {
return Collections.unmodifiableList(noFingerprints);
}
/**
* Makes sure that the data fields are up to date.
*/
private synchronized void upToDateCheck() {
// up to date check
if(lastUpdated>lastChanged) return;
lastUpdated = lastChanged+1;
int failCount = 0;
int totalCount = 0;
List<AbstractTestResultAction> individuals = new ArrayList<AbstractTestResultAction>();
List<AbstractProject> didntRun = new ArrayList<AbstractProject>();
List<AbstractProject> noFingerprints = new ArrayList<AbstractProject>();
for (AbstractProject job : getJobs()) {
RangeSet rs = owner.getDownstreamRelationship(job);
if(rs.isEmpty()) {
// is this job expected to produce a test result?
Run b;
if (includeFailedBuilds) {
b = job.getLastBuild();
} else {
b = job.getLastSuccessfulBuild();
}
if(b!=null && b.getAction(AbstractTestResultAction.class)!=null) {
if(b.getAction(FingerprintAction.class)!=null) {
didntRun.add(job);
} else {
noFingerprints.add(job);
}
}
} else {
for (int n : rs.listNumbersReverse()) {
Run b = job.getBuildByNumber(n);
if(b==null) continue;
Result targetResult;
if (includeFailedBuilds) {
targetResult = Result.FAILURE;
} else {
targetResult = Result.UNSTABLE;
}
if(b.isBuilding() || b.getResult().isWorseThan(targetResult))
continue; // don't count them
for( AbstractTestResultAction ta : b.getActions(AbstractTestResultAction.class)) {
failCount += ta.getFailCount();
totalCount += ta.getTotalCount();
individuals.add(ta);
}
break;
}
}
}
this.failCount = failCount;
this.totalCount = totalCount;
this.individuals = individuals;
this.didntRun = didntRun;
this.noFingerprints = noFingerprints;
}
public boolean getHasFingerprintAction() {
return this.owner.getAction(FingerprintAction.class)!=null;
}
@Override
public String getDisplayName() {
return Messages.AggregatedTestResultPublisher_Title();
}
@Override
public String getUrlName() {
return "aggregatedTestReport";
}
@Extension
public static class RunListenerImpl extends RunListener<Run> {
@Override
public void onCompleted(Run run, TaskListener listener) {
lastChanged = System.currentTimeMillis();
}
}
}
@Extension
public static final class DescriptorImpl extends BuildStepDescriptor<Publisher> {
public boolean isApplicable(Class<? extends AbstractProject> jobType) {
return true; // for all types
}
public String getDisplayName() {
return Messages.AggregatedTestResultPublisher_DisplayName();
}
@Override
public String getHelpFile() {
return "/help/tasks/aggregate-test/help.html";
}
public FormValidation doCheck(@AncestorInPath AbstractProject project, @QueryParameter String value) {
// Require CONFIGURE permission on this project
if(!project.hasPermission(Item.CONFIGURE)) return FormValidation.ok();
for (String name : Util.tokenize(fixNull(value), ",")) {
name = name.trim();
if(Jenkins.getInstance().getItem(name,project)==null)
return FormValidation.error(hudson.tasks.Messages.BuildTrigger_NoSuchProject(name,AbstractProject.findNearest(name).getName()));
}
return FormValidation.ok();
}
@Override
public AggregatedTestResultPublisher newInstance(StaplerRequest req, JSONObject formData) throws FormException {
JSONObject s = formData.getJSONObject("specify");
if(s.isNullObject())
return new AggregatedTestResultPublisher(null, req.getParameter("includeFailedBuilds") != null);
else
return new AggregatedTestResultPublisher(s.getString("jobs"), req.getParameter("includeFailedBuilds") != null);
}
public AutoCompletionCandidates doAutoCompleteJobs(@QueryParameter String value, @AncestorInPath Item self, @AncestorInPath ItemGroup container) {
return AutoCompletionCandidates.ofJobNames(Job.class,value,self,container);
}
}
@Restricted(NoExternalUse.class)
public static final class TestResultProjectAction extends InvisibleAction {
public final AbstractProject<?, ?> project;
private TestResultProjectAction(AbstractProject<?,?> project) {
this.project = project;
}
}
}
/*
* The MIT License
*
* Copyright (c) 2004-2009, Sun Microsystems, Inc.
*
* 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 hudson.tasks.test;
import hudson.AbortException;
import hudson.FilePath;
import hudson.FilePath.FileCallable;
import hudson.Launcher;
import hudson.Util;
import hudson.model.AbstractBuild;
import hudson.model.TaskListener;
import hudson.remoting.VirtualChannel;
import java.io.File;
import java.io.IOException;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
/**
* Default partial implementation of {@link TestResultParser} that handles GLOB dereferencing
* and other checks for user errors, such as misconfigured GLOBs, up-to-date checks on test reports.
*
* <p>
* The instance of the parser will be serialized to the node that performed the build and the parsing will be done
* remotely on that slave.
*
* @since 1.343
* @author Kohsuke Kawaguchi
*/
public abstract class DefaultTestResultParserImpl extends TestResultParser implements Serializable {
/**
* This method is executed on the slave that has the report files to parse test reports and builds {@link TestResult}.
*
* @param reportFiles
* List of files to be parsed. Never be empty nor null.
* @param launcher
* Can be used to fork processes on the machine where the build is running. Never null.
* @param listener
* Use this to report progress and other problems. Never null.
*
* @throws InterruptedException
* If the user cancels the build, it will be received as a thread interruption. Do not catch
* it, and instead just forward that through the call stack.
* @throws IOException
* If you don't care about handling exceptions gracefully, you can just throw IOException
* and let the default exception handling in Hudson takes care of it.
* @throws AbortException
* If you encounter an error that you handled gracefully, throw this exception and Hudson
* will not show a stack trace.
*/
protected abstract TestResult parse(List<File> reportFiles, Launcher launcher, TaskListener listener) throws InterruptedException, IOException;
@Override
public TestResult parse(final String testResultLocations, final AbstractBuild build, final Launcher launcher, final TaskListener listener) throws InterruptedException, IOException {
return build.getWorkspace().act(new FileCallable<TestResult>() {
final boolean ignoreTimestampCheck = IGNORE_TIMESTAMP_CHECK; // so that the property can be set on the master
final long buildTime = build.getTimestamp().getTimeInMillis();
final long nowMaster = System.currentTimeMillis();
public TestResult invoke(File dir, VirtualChannel channel) throws IOException, InterruptedException {
final long nowSlave = System.currentTimeMillis();
// files older than this timestamp is considered stale
long localBuildTime = buildTime + (nowSlave - nowMaster);
FilePath[] paths = new FilePath(dir).list(testResultLocations);
if (paths.length==0)
throw new AbortException("No test reports that matches "+testResultLocations+" found. Configuration error?");
// since dir is local, paths all point to the local files
List<File> files = new ArrayList<File>(paths.length);
for (FilePath path : paths) {
File report = new File(path.getRemote());
if (ignoreTimestampCheck || localBuildTime - 3000 /*error margin*/ < report.lastModified()) {
// this file is created during this build
files.add(report);
}
}
if (files.isEmpty()) {
// none of the files were new
throw new AbortException(
String.format(
"Test reports were found but none of them are new. Did tests run? %n"+
"For example, %s is %s old%n", paths[0].getRemote(),
Util.getTimeSpanString(localBuildTime-paths[0].lastModified())));
}
return parse(files,launcher,listener);
}
});
}
private static final long serialVersionUID = 1L;
public static final boolean IGNORE_TIMESTAMP_CHECK = Boolean.getBoolean(TestResultParser.class.getName()+".ignoreTimestampCheck");
}
/*
* The MIT License
*
* Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, Yahoo!, Inc.
*
* 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 hudson.tasks.test;
import java.util.Collection;
/**
* The purpose of this class is to provide a good place for the
* jelly to bind to.
* {@link TabulatedResult} whose immediate children
* are other {@link TabulatedResult}s.
*
* @author Kohsuke Kawaguchi
*/
public abstract class MetaTabulatedResult extends TabulatedResult {
/**
* All failed tests.
*/
public abstract Collection<? extends TestResult> getFailedTests();
}
/*
* The MIT License
*
* Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, Daniel Dyer, Tom Huybrechts, Yahoo!, Inc.
*
* 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 hudson.tasks.test;
import java.util.Collection;
/**
* Cumulated result of multiple tests.
*
* <p>
* On top of {@link TestResult}, this class introduces a tree structure
* of {@link TestResult}s.
*
* @author Kohsuke Kawaguchi
*/
public abstract class TabulatedResult extends TestResult {
/**
* Gets the child test result objects.
*
* @see TestObject#getParent()
*/
public abstract Collection<? extends TestResult> getChildren();
public abstract boolean hasChildren();
public String getChildTitle() {
return "";
}
}
/*
* The MIT License
*
* Copyright (c) 2009, Yahoo!, Inc.
*
* 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 hudson.tasks.test;
import hudson.AbortException;
import hudson.ExtensionList;
import hudson.ExtensionPoint;
import hudson.Launcher;
import hudson.model.AbstractBuild;
import jenkins.model.Jenkins;
import hudson.model.TaskListener;
import hudson.tasks.Publisher;
import java.io.IOException;
/**
* Parses test result files and builds in-memory representation of it as {@link TestResult}.
*
* <p>
* This extension point encapsulates the knowledge of a particular test report format and its parsing process,
* thereby improving the pluggability of test result parsing; integration with a new test tool can be done
* by just writing a parser, without writing a custom {@link Publisher}, and the test reports are displayed
* with the default UI and recognized by the rest of Hudson as test reports.
*
* <p>
* Most typical implementations of this class should extend from {@link DefaultTestResultParserImpl},
* which handles a set of default error checks on user inputs.
*
* <p>
* Parsers are stateless, and the {@link #parse(String, AbstractBuild, Launcher, TaskListener)} method
* can be concurrently invoked by multiple threads for different builds.
*
* @since 1.343
* @see DefaultTestResultParserImpl
*/
public abstract class TestResultParser implements ExtensionPoint {
/**
* Returns a human readable name of the parser, like "JUnit Parser".
*/
public String getDisplayName() {
return "Unknown Parser";
}
/**
* This text is used in the UI prompt for the GLOB that specifies files to be parsed by this parser.
* For example, "JUnit XML reports:"
*/
public String getTestResultLocationMessage() {
return "Paths to results files to parse:";
}
/**
* All registered {@link TestResultParser}s
*/
public static ExtensionList<TestResultParser> all() {
return ExtensionList.lookup(TestResultParser.class);
}
/**
* Parses the specified set of files and builds a {@link TestResult} object that represents them.
*
* <p>
* The implementation is encouraged to do the following:
*
* <ul>
* <li>
* If the build is successful but GLOB didn't match anything, report that as an error. This is
* to detect the error in GLOB. But don't do this if the build has already failed (for example,
* think of a failure in SCM checkout.)
*
* <li>
* Examine time stamp of test report files and if those are younger than the build, ignore them.
* This is to ignore test reports created by earlier executions. Take the possible timestamp
* difference in the master/slave into account.
* </ul>
*
* @param testResultLocations
* GLOB pattern relative to the {@linkplain AbstractBuild#getWorkspace() workspace} that
* specifies the locations of the test result files. Never null.
* @param build
* Build for which these tests are parsed. Never null.
* @param launcher
* Can be used to fork processes on the machine where the build is running. Never null.
* @param listener
* Use this to report progress and other problems. Never null.
*
* @throws InterruptedException
* If the user cancels the build, it will be received as a thread interruption. Do not catch
* it, and instead just forward that through the call stack.
* @throws IOException
* If you don't care about handling exceptions gracefully, you can just throw IOException
* and let the default exception handling in Hudson takes care of it.
* @throws AbortException
* If you encounter an error that you handled gracefully, throw this exception and Hudson
* will not show a stack trace.
*/
public abstract TestResult parse(String testResultLocations,
AbstractBuild build, Launcher launcher,
TaskListener listener)
throws InterruptedException, IOException;
}
# This file is under the MIT License by authors
Error\ Message=\u05D4\u05D5\u05D3\u05E2\u05EA \u05E9\u05D2\u05D9\u05D0\u05D4
Standard\ Output=\u05E4\u05DC\u05D8 \u05E1\u05D8\u05E0\u05D3\u05E8\u05D8\u05D9
failingFor=\u05E0\u05DB\u05E9\u05DC \u05DE\u05D0\u05D6 {0} {0,choice,0#\u05D1\u05E0\u05D9\u05D5\u05EA|1#\u05D4\u05D1\u05E0\u05D9\u05D4 \u05D4\u05D0\u05D7\u05E8\u05D5\u05E0\u05D4|1<\u05D1\u05E0\u05D9\u05D5\u05EA}
since.after='' ''
since.before=\u05D4\u05D7\u05DC \u05DE-'' ''
took=\u05E0\u05DE\u05E9\u05DA {0}.
此差异已折叠。
此差异已折叠。
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册