提交 a1e93bf5 编写于 作者: H huybrechts

Merging in the junit branch. Lots of new features and bugfixes, including:

- support for TestAction that can contribute various things to any TestObject (packages, classes and methods), using the TestDataPublisher extension point. 
Features include UI on the TestObject page, badges on the overview page, tasks in the side panel and annotating test output.
- a junit-attachments plugin that uses this to record test attachments (files)
- an extension to the claim plugin that offers claims on failing tests
- editable test descriptions (on any TestObject)
- a history page for all TestObjects (including test count and duration graph)
- error details for failed tests are shown in a collapsed panel in test overview pages (package+class)
- [FIXED HUDSON-3149] if file is .xml but does not parse, record a fake failing test
- [FIXED HUDSON-1820] extra column displaying skipped tests
- [HUDSON-2046] new test packages, classes and methods are shown in bold
- [FIXED HUDSON-2228] test duration charts on all levels
- [FIXED HUDSON-3451] expand environment variables in test results path




git-svn-id: https://hudson.dev.java.net/svn/hudson/trunk/hudson/main@20580 71c3de6d-444a-0410-be80-ed276b4c234a
上级 0246cc62
/*
* The MIT License
*
* Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, Daniel Dyer, Seiji Sogabe
* Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, Daniel Dyer, Seiji Sogabe, 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
......@@ -24,12 +24,14 @@
package hudson.tasks.junit;
import hudson.model.AbstractBuild;
import org.dom4j.Element;
import org.kohsuke.stapler.export.Exported;
import hudson.model.Run;
import java.util.Comparator;
import java.text.DecimalFormat;
import java.text.ParseException;
import java.util.Comparator;
import org.dom4j.Element;
import org.kohsuke.stapler.export.Exported;
/**
* One test result.
......@@ -139,6 +141,10 @@ public final class CaseResult extends TestObject implements Comparable<CaseResul
this.stdout = stdout;
this.stderr = stderr;
}
public ClassResult getParent() {
return classResult;
}
private static String getError(Element testCase) {
String msg = testCase.elementText("error");
......@@ -233,6 +239,22 @@ public final class CaseResult extends TestObject implements Comparable<CaseResul
return className+'.'+getName();
}
@Override
public int getFailCount() {
if (!isPassed() && !isSkipped()) return 1; else return 0;
}
@Override
public int getSkipCount() {
if (isSkipped()) return 1; else return 0;
}
@Override
public int getPassCount() {
return isPassed() ? 1 : 0;
}
/**
* If this test failed, then return the build number
* when this test started failing.
......@@ -241,6 +263,10 @@ public final class CaseResult extends TestObject implements Comparable<CaseResul
public int getFailedSince() {
return failedSince;
}
public Run<?,?> getFailedSinceRun() {
return getOwner().getParent().getBuildByNumber(failedSince);
}
/**
* Gets the number of consecutive builds (including this)
......@@ -270,7 +296,7 @@ public final class CaseResult extends TestObject implements Comparable<CaseResul
@Exported
public String getStdout() {
if(stdout!=null) return stdout;
return getParent().getStdout();
return getSuiteResult().getStdout();
}
/**
......@@ -282,7 +308,7 @@ public final class CaseResult extends TestObject implements Comparable<CaseResul
@Exported
public String getStderr() {
if(stderr!=null) return stderr;
return getParent().getStderr();
return getSuiteResult().getStderr();
}
@Override
......@@ -291,6 +317,13 @@ public final class CaseResult extends TestObject implements Comparable<CaseResul
if(pr==null) return null;
return pr.getCase(getName());
}
@Override
public CaseResult getResultInBuild(AbstractBuild<?, ?> build) {
ClassResult pr = getParent().getResultInBuild(build);
if(pr==null) return null;
return pr.getCaseResult(getName());
}
/**
* If there was an error or a failure, this is the stack trace, or otherwise null.
......@@ -326,12 +359,25 @@ public final class CaseResult extends TestObject implements Comparable<CaseResul
return skipped;
}
public SuiteResult getParent() {
public SuiteResult getSuiteResult() {
return parent;
}
public String annotate(String text) {
if (text == null)
return null;
text = text.replace("&", "&amp;").replace("<", "&lt;").replaceAll(
"\\b(https?://[^\\s)>]+)", "<a href=\"$1\">$1</a>");
for (TestAction action: getTestActions()) {
text = action.annotate(text);
}
return text;
}
public AbstractBuild<?,?> getOwner() {
return parent.getParent().getOwner();
return getSuiteResult().getParent().getOwner();
}
/**
......
/*
* The MIT License
*
* Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, Daniel Dyer, id:cactusman
* Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, Daniel Dyer, id:cactusman, 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
......@@ -38,7 +38,7 @@ import java.util.List;
* @author Kohsuke Kawaguchi
*/
public final class ClassResult extends TabulatedResult implements Comparable<ClassResult> {
private final String className;
private final String className; // simple name
private final List<CaseResult> cases = new ArrayList<CaseResult>();
......@@ -57,14 +57,17 @@ public final class ClassResult extends TabulatedResult implements Comparable<Cla
return parent;
}
public AbstractBuild<?,?> getOwner() {
return parent.getOwner();
}
public ClassResult getPreviousResult() {
PackageResult pr = parent.getPreviousResult();
if(pr==null) return null;
return pr.getDynamic(getName(),null,null);
return pr.getClassResult(getName());
}
@Override
public ClassResult getResultInBuild(AbstractBuild<?, ?> build) {
PackageResult pr = getParent().getResultInBuild(build);
if(pr==null) return null;
return pr.getClassResult(getName());
}
public String getTitle() {
......@@ -81,8 +84,8 @@ public final class ClassResult extends TabulatedResult implements Comparable<Cla
public @Override String getSafeName() {
return uniquifyName(parent.getChildren(), safe(getName()));
}
public CaseResult getDynamic(String name, StaplerRequest req, StaplerResponse rsp) {
public CaseResult getCaseResult(String name) {
for (CaseResult c : cases) {
if(c.getSafeName().equals(name))
return c;
......@@ -90,6 +93,15 @@ public final class ClassResult extends TabulatedResult implements Comparable<Cla
return null;
}
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() {
......@@ -139,6 +151,9 @@ public final class ClassResult extends TabulatedResult implements Comparable<Cla
Collections.sort(cases);
}
public String getClassName() {
return className;
}
public int compareTo(ClassResult that) {
return this.className.compareTo(that.className);
......@@ -147,4 +162,27 @@ public final class ClassResult extends TabulatedResult implements Comparable<Cla
public String getDisplayName() {
return getName();
}
public String getFullName() {
return getParent().getDisplayName() + "." + className;
}
/**
* Gets the relative path to this test case from the given object.
*/
public String getRelativePathFrom(TestObject it) {
if(it==this)
return ".";
if (it instanceof TestResult) {
return getParent().getSafeName() + "/" + getSafeName();
} else if (it instanceof PackageResult) {
return getSafeName();
} else if (it instanceof CaseResult) {
return "..";
} else {
throw new UnsupportedOperationException();
}
}
}
package hudson.tasks.junit;
import hudson.model.AbstractBuild;
import hudson.util.ChartUtil;
import hudson.util.ColorPalette;
import hudson.util.DataSetBuilder;
import hudson.util.ShiftedCategoryAxis;
import hudson.util.StackedAreaRenderer2;
import java.awt.Color;
import java.awt.Paint;
import java.io.IOException;
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.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
public class History {
private final TestObject testObject;
public History(TestObject testObject) {
this.testObject = testObject;
}
public TestObject getTestObject() {
return testObject;
}
public List<TestObject> getList() {
List<TestObject> list = new ArrayList<TestObject>();
for (AbstractBuild<?,?> b: testObject.getOwner().getParent().getBuilds()) {
if (b.isBuilding()) continue;
TestObject o = testObject.getResultInBuild(b);
if (o != null) {
list.add(o);
}
}
return list;
}
public void doDurationGraph(StaplerRequest req, StaplerResponse rsp) throws IOException {
if (req.checkIfModified(testObject.getOwner().getTimestamp(), rsp)) return;
ChartUtil.generateGraph(req, rsp, createGraph(getDurationData(), "seconds"), 500, 400);
}
public void doDurationMap(StaplerRequest req, StaplerResponse rsp) throws IOException {
if (req.checkIfModified(testObject.getOwner().getTimestamp(), rsp)) return;
ChartUtil.generateClickableMap(req, rsp, createGraph(getDurationData(), ""), 500, 400);
}
private DataSetBuilder<String, ChartLabel> getDurationData() {
DataSetBuilder<String, ChartLabel> data = new DataSetBuilder<String, ChartLabel>();
for (TestObject o: getList()) {
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;
}
private DataSetBuilder<String, ChartLabel> getCountData() {
DataSetBuilder<String, ChartLabel> data = new DataSetBuilder<String, ChartLabel>();
for (TestObject o: getList()) {
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;
}
public void doCountGraph(StaplerRequest req, StaplerResponse rsp) throws IOException {
if (req.checkIfModified(testObject.getOwner().getTimestamp(), rsp)) return;
ChartUtil.generateGraph(req, rsp, createGraph(getCountData(), ""), 500, 400);
}
public void doCountMap(StaplerRequest req, StaplerResponse rsp) throws IOException {
if (req.checkIfModified(testObject.getOwner().getTimestamp(), rsp)) return;
ChartUtil.generateClickableMap(req, rsp, createGraph(getCountData(), ""), 500, 400);
}
private JFreeChart createGraph(DataSetBuilder<String, ChartLabel> data, String yLabel) {
final CategoryDataset dataset = data.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 String.valueOf(label.o.getOwner().number);
}
@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.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;
}
class ChartLabel implements Comparable<ChartLabel> {
TestObject o;
public ChartLabel(TestObject o) {
this.o = o;
}
public int compareTo(ChartLabel that) {
int result = this.o.getOwner().number - that.o.getOwner().number;
return result;
}
public boolean equals(Object o) {
if (!(o instanceof ChartLabel)) {
return false;
}
ChartLabel that = (ChartLabel) o;
boolean result = this.o == that.o;
return result;
}
public Color getColor() {
return null;
}
public int hashCode() {
return o.hashCode();
}
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();
}
}
}
/*
* The MIT License
*
* Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, Martin Eigenbrodt
* Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, Martin Eigenbrodt, 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
......@@ -23,12 +23,12 @@
*/
package hudson.tasks.junit;
import hudson.AbortException;
import hudson.Extension;
import hudson.FilePath;
import hudson.FilePath.FileCallable;
import hudson.Launcher;
import hudson.Util;
import hudson.AbortException;
import hudson.FilePath.FileCallable;
import hudson.matrix.MatrixAggregatable;
import hudson.matrix.MatrixAggregator;
import hudson.matrix.MatrixBuild;
......@@ -36,146 +36,220 @@ import hudson.model.AbstractBuild;
import hudson.model.AbstractProject;
import hudson.model.Action;
import hudson.model.BuildListener;
import hudson.model.Result;
import hudson.model.CheckPoint;
import hudson.model.Descriptor;
import hudson.model.Result;
import hudson.model.Saveable;
import hudson.remoting.VirtualChannel;
import hudson.tasks.BuildStepDescriptor;
import hudson.tasks.BuildStepMonitor;
import hudson.tasks.Publisher;
import hudson.tasks.Recorder;
import hudson.tasks.BuildStepMonitor;
import hudson.tasks.junit.TestResultAction.Data;
import hudson.tasks.test.TestResultAggregator;
import hudson.tasks.test.TestResultProjectAction;
import hudson.util.DescribableList;
import hudson.util.FormValidation;
import java.io.File;
import java.io.IOException;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
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 java.io.File;
import java.io.IOException;
import java.io.Serializable;
import org.kohsuke.stapler.StaplerRequest;
/**
* Generates HTML report from JUnit test result XML files.
*
*
* @author Kohsuke Kawaguchi
*/
public class JUnitResultArchiver extends Recorder implements Serializable, MatrixAggregatable {
/**
* {@link FileSet} "includes" string, like "foo/bar/*.xml"
*/
private final String testResults;
@DataBoundConstructor
public JUnitResultArchiver(String testResults) {
this.testResults = testResults;
}
public boolean perform(AbstractBuild build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException {
listener.getLogger().println(Messages.JUnitResultArchiver_Recording());
TestResultAction action;
try {
final long buildTime = build.getTimestamp().getTimeInMillis();
final long nowMaster = System.currentTimeMillis();
TestResult result = build.getWorkspace().act(new FileCallable<TestResult>() {
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());
}
return parseResult(ds, buildTime + (nowSlave - nowMaster));
}
});
CHECKPOINT.block();
action = new TestResultAction(build, result, listener);
if(result.getPassCount()==0 && result.getFailCount()==0)
throw new AbortException(Messages.JUnitResultArchiver_ResultIsEmpty());
} 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.getActions().add(action);
CHECKPOINT.report();
if(action.getResult().getFailCount()>0)
build.setResult(Result.UNSTABLE);
return true;
}
protected TestResult parseResult(DirectoryScanner ds, long buildTime) throws IOException {
return new TestResult(buildTime, ds);
}
/**
* This class does explicit checkpointing.
*/
public BuildStepMonitor getRequiredMonitorService() {
return BuildStepMonitor.NONE;
}
public String getTestResults() {
return testResults;
}
@Override
public Action getProjectAction(AbstractProject<?, ?> project) {
return new TestResultProjectAction(project);
}
public MatrixAggregator createAggregator(MatrixBuild build, Launcher launcher, BuildListener listener) {
return new TestResultAggregator(build,launcher,listener);
}
/**
* Test result tracks the diff from the previous run, hence the checkpoint.
*/
private static final CheckPoint CHECKPOINT = new CheckPoint("JUnit result archiving");
private static final long serialVersionUID = 1L;
@Extension
public static class DescriptorImpl extends BuildStepDescriptor<Publisher> {
public String getDisplayName() {
return Messages.JUnitResultArchiver_DisplayName();
}
public String getHelpFile() {
return "/help/tasks/junit/report.html";
}
/**
* 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 class JUnitResultArchiver extends Recorder implements Serializable,
MatrixAggregatable {
/**
* {@link FileSet} "includes" string, like "foo/bar/*.xml"
*/
private final String testResults;
private final DescribableList<TestDataPublisher, Descriptor<TestDataPublisher>> testDataPublishers;
/**
* left for backwards compatibility
*
* @param testResults
*/
@Deprecated
public JUnitResultArchiver(String testResults) {
this(testResults, null);
}
@DataBoundConstructor
public JUnitResultArchiver(
String testResults,
DescribableList<TestDataPublisher, Descriptor<TestDataPublisher>> testDataPublishers) {
this.testResults = testResults;
this.testDataPublishers = testDataPublishers;
}
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 {
final long buildTime = build.getTimestamp().getTimeInMillis();
final long nowMaster = System.currentTimeMillis();
TestResult result = build.getWorkspace().act(
new FileCallable<TestResult>() {
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());
}
return parseResult(ds, buildTime
+ (nowSlave - nowMaster));
}
});
action = new TestResultAction(build, result, listener);
if (result.getPassCount() == 0 && result.getFailCount() == 0)
throw new AbortException(Messages
.JUnitResultArchiver_ResultIsEmpty());
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);
CHECKPOINT.block();
} 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.getActions().add(action);
CHECKPOINT.report();
if (action.getResult().getFailCount() > 0)
build.setResult(Result.UNSTABLE);
return true;
}
protected TestResult parseResult(DirectoryScanner ds, long buildTime)
throws IOException {
return new TestResult(buildTime, ds);
}
/**
* This class does explicit checkpointing.
*/
public BuildStepMonitor getRequiredMonitorService() {
return BuildStepMonitor.NONE;
}
public String getTestResults() {
return testResults;
}
public DescribableList<TestDataPublisher, Descriptor<TestDataPublisher>> getTestDataPublishers() {
return testDataPublishers;
}
@Override
public Action getProjectAction(AbstractProject<?, ?> project) {
return new TestResultProjectAction(project);
}
public MatrixAggregator createAggregator(MatrixBuild build,
Launcher launcher, BuildListener listener) {
return new TestResultAggregator(build, launcher, listener);
}
/**
* Test result tracks the diff from the previous run, hence the checkpoint.
*/
private static final CheckPoint CHECKPOINT = new CheckPoint(
"JUnit result archiving");
private static final long serialVersionUID = 1L;
@Extension
public static class DescriptorImpl extends BuildStepDescriptor<Publisher> {
public String getDisplayName() {
return Messages.JUnitResultArchiver_DisplayName();
}
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");
DescribableList<TestDataPublisher, Descriptor<TestDataPublisher>> testDataPublishers = new DescribableList<TestDataPublisher, Descriptor<TestDataPublisher>>(
new Saveable() {
public void save() throws IOException {
// no-op
}
});
testDataPublishers.rebuild(req, formData, TestDataPublisher.all());
return new JUnitResultArchiver(testResults, 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;
}
}
}
/*
* The MIT License
*
* Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, Daniel Dyer, id:cactusman
* Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, Daniel Dyer, id:cactusman, 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
......@@ -57,6 +57,10 @@ public final class PackageResult extends MetaTabulatedResult implements Comparab
this.packageName = packageName;
this.parent = parent;
}
public TestResult getParent() {
return parent;
}
@Exported(visibility=999)
public String getName() {
......@@ -67,16 +71,19 @@ public final class PackageResult extends MetaTabulatedResult implements Comparab
return uniquifyName(parent.getChildren(), safe(getName()));
}
public AbstractBuild<?,?> getOwner() {
return parent.getOwner();
}
public PackageResult getPreviousResult() {
TestResult tr = parent.getPreviousResult();
if(tr==null) return null;
return tr.byPackage(getName());
}
@Override
public PackageResult getResultInBuild(AbstractBuild<?, ?> build) {
TestResult tr = parent.getResultInBuild(build);
if(tr==null) return null;
return tr.byPackage(getName());
}
public String getTitle() {
return Messages.PackageResult_getTitle(getName());
}
......@@ -105,10 +112,19 @@ public final class PackageResult extends MetaTabulatedResult implements Comparab
return skipCount;
}
public ClassResult getDynamic(String name, StaplerRequest req, StaplerResponse rsp) {
return classes.get(name);
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();
......@@ -128,7 +144,7 @@ public final class PackageResult extends MetaTabulatedResult implements Comparab
void add(CaseResult r) {
String n = r.getSimpleName(), sn = safe(n);
ClassResult c = classes.get(sn);
ClassResult c = getClassResult(sn);
if(c==null)
classes.put(sn,c=new ClassResult(this,n));
c.add(r);
......
/*
* The MIT License
*
* Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, Erik Ramfelt, Xavier Le Vourch
* Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, Erik Ramfelt, Xavier Le Vourch, 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
......@@ -23,6 +23,13 @@
*/
package hudson.tasks.junit;
import java.io.File;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
......@@ -30,11 +37,6 @@ import org.dom4j.io.SAXReader;
import org.kohsuke.stapler.export.Exported;
import org.kohsuke.stapler.export.ExportedBean;
import java.io.File;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
/**
* Result of one test suite.
*
......@@ -50,6 +52,7 @@ import java.util.List;
*/
@ExportedBean
public final class SuiteResult implements Serializable {
private final String file;
private final String name;
private final String stdout;
private final String stderr;
......@@ -70,6 +73,7 @@ public final class SuiteResult implements Serializable {
this.name = name;
this.stderr = stderr;
this.stdout = stdout;
this.file = null;
}
/**
......@@ -102,6 +106,7 @@ public final class SuiteResult implements Serializable {
}
private SuiteResult(File xmlReport, Element suite) throws DocumentException {
this.file = xmlReport.getAbsolutePath();
String name = suite.attributeValue("name");
if(name==null)
// some user reported that name is null in their environment.
......@@ -183,8 +188,15 @@ public final class SuiteResult implements Serializable {
public String getStderr() {
return stderr;
}
/**
* The absolute path to the original test report. OS-dependent.
*/
public String getFile() {
return file;
}
public TestResult getParent() {
public TestResult getParent() {
return parent;
}
......@@ -218,6 +230,14 @@ public final class SuiteResult implements Serializable {
}
return null;
}
public Set<String> getClassNames() {
Set<String> result = new HashSet<String>();
for (CaseResult c : cases) {
result.add(c.getClassName());
}
return result;
}
/*package*/ boolean freeze(TestResult owner) {
if(this.parent!=null)
......
/*
* The MIT License
*
* Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, Daniel Dyer
* Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, Daniel Dyer, 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
......@@ -37,28 +37,6 @@ public abstract class TabulatedResult extends TestObject {
*/
public abstract String getTitle();
/**
* 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 final int getTotalCount() {
return getPassCount()+getFailCount()+getSkipCount();
}
/**
* Gets the child test result objects.
*/
......
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
*
*/
public abstract class TestAction implements Action {
/**
* Returns text with annotations.
*/
public String annotate(String text) {
return text;
}
}
package hudson.tasks.junit;
import java.io.IOException;
import hudson.DescriptorExtensionList;
import hudson.ExtensionPoint;
import hudson.Launcher;
import hudson.model.AbstractBuild;
import hudson.model.BuildListener;
import hudson.model.Describable;
import hudson.model.Descriptor;
import hudson.model.Hudson;
public abstract class TestDataPublisher implements
Describable<TestDataPublisher>, ExtensionPoint {
public abstract TestResultAction.Data getTestData(
AbstractBuild<?, ?> build, Launcher launcher,
BuildListener listener, TestResult testResult) throws IOException, InterruptedException;
@SuppressWarnings("unchecked")
public Descriptor<TestDataPublisher> getDescriptor() {
return Hudson.getInstance().getDescriptor(getClass());
}
public static DescriptorExtensionList<TestDataPublisher, Descriptor<TestDataPublisher>> all() {
return Hudson.getInstance().getDescriptorList(
TestDataPublisher.class);
}
}
/*
* The MIT License
*
* Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi
* Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, 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
......@@ -23,90 +23,232 @@
*/
package hudson.tasks.junit;
import hudson.Util;
import hudson.model.AbstractBuild;
import hudson.model.ModelObject;
import hudson.model.AbstractModelObject;
import hudson.model.Action;
import hudson.model.Api;
import hudson.Util;
import hudson.model.Build;
import hudson.model.Item;
import hudson.model.Messages;
import hudson.model.Result;
import hudson.model.Run;
import hudson.util.ChartUtil;
import hudson.util.ColorPalette;
import hudson.util.DataSetBuilder;
import hudson.util.ShiftedCategoryAxis;
import hudson.util.StackedAreaRenderer2;
import java.awt.Color;
import java.awt.Paint;
import java.io.IOException;
import java.io.Serializable;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.WeakHashMap;
import javax.servlet.ServletException;
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.HttpRedirect;
import org.kohsuke.stapler.HttpResponse;
import org.kohsuke.stapler.QueryParameter;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import org.kohsuke.stapler.export.ExportedBean;
/**
* Base class for all test result objects.
*
*
* @author Kohsuke Kawaguchi
*/
@ExportedBean
public abstract class TestObject implements ModelObject, Serializable {
public abstract AbstractBuild<?,?> getOwner();
public abstract class TestObject extends AbstractModelObject implements Serializable {
public AbstractBuild<?, ?> getOwner() {
return getParent().getOwner();
}
/**
* Gets the counter part of this {@link TestObject} in the previous run.
*
* @return null
* if no such counter part exists.
*/
public abstract TestObject getPreviousResult();
private volatile transient String id;
/**
* Time took to run this test. In seconds.
*/
public abstract float getDuration();
public abstract TestObject getParent();
/**
* Returns the string representation of the {@link #getDuration()},
* in a human readable format.
*/
public String getDurationString() {
return Util.getTimeSpanString((long)(getDuration()*1000));
}
public String getId() {
if (id == null) {
id = getParent().getId() + "/" + getSafeName();
}
return id;
}
/**
* Returns url relative to TestResult
*/
public String getUrl() {
return getId();
}
/**
* Exposes this object through the remote API.
*/
public Api getApi() {
return new Api(this);
}
public TestResult getTestResult() {
return getParent().getTestResult();
}
public TestResultAction getTestResultAction() {
return getOwner().getAction(TestResultAction.class);
}
public List<TestAction> getTestActions() {
return getTestResultAction().getActions(this);
}
public <T> T getTestAction(Class<T> klazz) {
for (TestAction action : getTestActions()) {
if (klazz.isAssignableFrom(action.getClass())) {
return klazz.cast(action);
}
}
return null;
}
/**
* 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 String getDurationString() {
return Util.getTimeSpanString((long) (getDuration() * 1000));
}
public String getDescription() {
return getTestResultAction().getDescription(this);
}
public void setDescription(String description) {
getTestResultAction().setDescription(this, description);
}
/**
* Exposes this object through the remote API.
*/
public Api getApi() {
return new Api(this);
}
/**
* Gets the name of this object.
*/
public/* abstract */String getName() {
return "";
}
/**
* Gets the version of {@link #getName()} that's URL-safe.
*/
public String getSafeName() {
return safe(getName());
}
public String getSearchUrl() {
return getSafeName();
}
/**
* #2988: uniquifies a {@link #getSafeName} amongst children of the parent.
*/
protected final synchronized String uniquifyName(
Collection<? extends TestObject> siblings, String base) {
String uniquified = base;
int sequence = 1;
for (TestObject sibling : siblings) {
if (sibling != this
&& uniquified.equals(UNIQUIFIED_NAMES.get(sibling))) {
uniquified = base + '_' + ++sequence;
}
}
UNIQUIFIED_NAMES.put(this, uniquified);
return uniquified;
}
private static final Map<TestObject, String> UNIQUIFIED_NAMES = new WeakHashMap<TestObject, String>();
/**
* Replaces URL-unsafe characters.
*/
protected static String safe(String s) {
// 3 replace calls is still 2-3x faster than a regex replaceAll
return s.replace('/', '_').replace('\\', '_').replace(':', '_');
}
/**
* Gets the name of this object.
* Gets the total number of passed tests.
*/
public /*abstract*/ String getName() {return "";}
public abstract int getPassCount();
/**
* Gets the version of {@link #getName()} that's URL-safe.
* Gets the total number of failed tests.
*/
public String getSafeName() {
return safe(getName());
}
public abstract int getFailCount();
/**
* #2988: uniquifies a {@link #getSafeName} amongst children of the parent.
* Gets the total number of skipped tests.
*/
protected final synchronized String uniquifyName(Collection<? extends TestObject> siblings, String base) {
String uniquified = base;
int sequence = 1;
for (TestObject sibling : siblings) {
if (sibling != this && uniquified.equals(UNIQUIFIED_NAMES.get(sibling))) {
uniquified = base + '_' + ++sequence;
}
}
UNIQUIFIED_NAMES.put(this, uniquified);
return uniquified;
}
private static final Map<TestObject,String> UNIQUIFIED_NAMES = new WeakHashMap<TestObject,String>();
public abstract int getSkipCount();
/**
* Replaces URL-unsafe characters.
* Gets the total number of tests.
*/
protected static String safe(String s) {
// 3 replace calls is still 2-3x faster than a regex replaceAll
return s.replace('/','_').replace('\\', '_').replace(':','_');
public final int getTotalCount() {
return getPassCount()+getFailCount()+getSkipCount();
}
public History getHistory() {
return new History(this);
}
public Object getDynamic(String token, StaplerRequest req,
StaplerResponse rsp) {
for (Action a : getTestActions()) {
if (a == null)
continue; // be defensive
String urlName = a.getUrlName();
if (urlName == null)
continue;
if (urlName.equals(token))
return a;
}
return null;
}
public synchronized HttpResponse doSubmitDescription(
@QueryParameter String description) throws IOException,
ServletException {
getOwner().checkPermission(Item.BUILD);
setDescription(description);
getOwner().save();
return new HttpRedirect(".");
}
private static final long serialVersionUID = 1L;
private static final long serialVersionUID = 1L;
}
/*
* The MIT License
*
* Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, Daniel Dyer, id:cactusman
* Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, Daniel Dyer, id:cactusman, 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
......@@ -23,17 +23,15 @@
*/
package hudson.tasks.junit;
import hudson.AbortException;
import hudson.Util;
import hudson.model.AbstractBuild;
import hudson.util.IOException2;
import hudson.*;
import org.apache.tools.ant.DirectoryScanner;
import org.dom4j.DocumentException;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import org.kohsuke.stapler.export.Exported;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
......@@ -42,6 +40,12 @@ import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import org.apache.tools.ant.DirectoryScanner;
import org.dom4j.DocumentException;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import org.kohsuke.stapler.export.Exported;
/**
* Root of all the test results for one build.
*
......@@ -80,7 +84,7 @@ public final class TestResult extends MetaTabulatedResult {
* Number of failed/error tests.
*/
private transient List<CaseResult> failedTests;
/**
* Creates an empty result.
*/
......@@ -94,6 +98,19 @@ public final class TestResult extends MetaTabulatedResult {
public TestResult(long buildTime, DirectoryScanner results) throws IOException {
parse(buildTime, results);
}
public TestObject getParent() {
return null;
}
public String getId() {
return "";
}
@Override
public TestResult getTestResult() {
return this;
}
/**
* Collect reports from the given {@link DirectoryScanner}, while
......@@ -164,11 +181,18 @@ public final class TestResult extends MetaTabulatedResult {
} catch (RuntimeException e) {
throw new IOException2("Failed to read "+reportFile,e);
} catch (DocumentException e) {
if(!reportFile.getPath().endsWith(".xml"))
if (!reportFile.getPath().endsWith(".xml")) {
throw new IOException2("Failed to read "+reportFile+"\n"+
"Is this really a JUnit report file? Your configuration must be matching too many files",e);
else
} else {
SuiteResult sr = new SuiteResult(reportFile.getName(), "", "");
StringWriter writer = new StringWriter();
e.printStackTrace(new PrintWriter(writer));
String error = "Failed to read test report file "+reportFile.getAbsolutePath()+"\n"+writer.toString();
sr.addCase(new CaseResult(sr,"<init>",error));
add(sr);
throw new IOException2("Failed to read "+reportFile,e);
}
}
}
......@@ -189,6 +213,13 @@ public final class TestResult extends MetaTabulatedResult {
return null;
}
@Override
public TestResult getResultInBuild(AbstractBuild<?, ?> build) {
TestResultAction tra = build.getAction(TestResultAction.class);
if (tra == null) return null;
return tra.getResult();
}
public String getTitle() {
return Messages.TestResult_getTitle();
}
......@@ -240,8 +271,13 @@ public final class TestResult extends MetaTabulatedResult {
return "";
}
public PackageResult getDynamic(String packageName, StaplerRequest req, StaplerResponse rsp) {
return byPackage(packageName);
public Object getDynamic(String token, StaplerRequest req, StaplerResponse rsp) {
PackageResult result = byPackage(token);
if (result != null) {
return result;
} else {
return super.getDynamic(token, req, rsp);
}
}
public PackageResult byPackage(String packageName) {
......@@ -251,7 +287,7 @@ public final class TestResult extends MetaTabulatedResult {
public SuiteResult getSuite(String name) {
return suitesByName.get(name);
}
/**
* Builds up the transient part of the data structure
* from results {@link #parse(File) parsed} so far.
......
/*
* The MIT License
*
* Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, Daniel Dyer, Red Hat, Inc.
* Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, Daniel Dyer, Red Hat, 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
......@@ -23,7 +23,6 @@
*/
package hudson.tasks.junit;
import com.thoughtworks.xstream.XStream;
import hudson.XmlFile;
import hudson.model.AbstractBuild;
import hudson.model.Action;
......@@ -31,16 +30,22 @@ import hudson.model.BuildListener;
import hudson.tasks.test.AbstractTestResultAction;
import hudson.util.StringConverter2;
import hudson.util.XStream2;
import java.util.List;
import org.kohsuke.stapler.StaplerProxy;
import org.kohsuke.stapler.export.Exported;
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.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.kohsuke.stapler.StaplerProxy;
import com.thoughtworks.xstream.XStream;
/**
* {@link Action} that displays the JUnit test result.
*
......@@ -58,7 +63,8 @@ public class TestResultAction extends AbstractTestResultAction<TestResultAction>
private int failCount;
private int skipCount;
private Integer totalCount;
private List<Data> testData;
private Map<String,String> descriptions = new ConcurrentHashMap<String, String>();
public TestResultAction(AbstractBuild owner, TestResult result, BuildListener listener) {
super(owner);
......@@ -154,9 +160,45 @@ public class TestResultAction extends AbstractTestResultAction<TestResultAction>
public Object getTarget() {
return getResult();
}
public String getDescription(TestObject object) {
return descriptions.get(object.getId());
}
public void setDescription(TestObject object, String description) {
descriptions.put(object.getId(), description);
}
public List<TestAction> getActions(TestObject object) {
List<TestAction> result = new ArrayList<TestAction>();
for (Data data: testData) {
result.addAll(data.getTestAction(object));
}
return Collections.unmodifiableList(result);
}
public void setData(List<Data> testData) {
this.testData = testData;
}
public static abstract class Data {
/**
* Returns all TestActions for the testObject. Always non-null
*/
public abstract List<TestAction> getTestAction(TestObject testObject);
}
public Object readResolve() {
if (descriptions == null) {
descriptions = new ConcurrentHashMap<String, String>();
}
if (testData == null) {
testData = new ArrayList<Data>();
}
return this;
}
private static final Logger logger = Logger.getLogger(TestResultAction.class.getName());
private static final XStream XSTREAM = new XStream2();
......@@ -168,4 +210,5 @@ public class TestResultAction extends AbstractTestResultAction<TestResultAction>
XSTREAM.registerConverter(new StringConverter2(),100);
}
}
<!--
The MIT License
Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi
Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, 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
......@@ -30,26 +30,7 @@ THE SOFTWARE.
<l:side-panel>
<l:tasks>
<j:set var="buildUrl" value="${h.decompose(request)}" />
<l:task icon="images/24x24/up.gif" href="${it.upUrl}" title="${%Back to Project}" />
<l:task icon="images/24x24/search.gif" href="${buildUrl.baseUrl}/" title="${%Status}" />
<l:task icon="images/24x24/notepad.gif" href="${buildUrl.baseUrl}/changes" title="${%Changes}" />
<j:choose>
<j:when test="${it.logFile.length() > 200000}">
<!-- Show raw link directly so user need not click through live console page. -->
<div class="task">
<a href="${buildUrl.baseUrl}/console">
<img width="24" height="24" style="margin: 2px;" src="${imagesURL}/24x24/terminal.gif" alt=""/>
</a>
<st:nbsp />
<a href="${buildUrl.baseUrl}/console">${%Console Output}</a>
<st:nbsp />
<a href="${buildUrl.baseUrl}/consoleText">[${%raw}]</a>
</div>
</j:when>
<j:otherwise>
<l:task icon="images/24x24/terminal.gif" href="${buildUrl.baseUrl}/console" title="${%Console Output}" />
</j:otherwise>
</j:choose>
<st:include page="tasks.jelly"/>
<st:include page="actions.jelly" />
<j:if test="${it.previousBuild!=null}">
<l:task icon="images/24x24/previous.gif" href="${buildUrl.previousBuildUrl}" title="${%Previous Build}" />
......
<!--
The MIT License
Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, 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.
-->
<!--
Side panel for the build view.
-->
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form" xmlns:i="jelly:fmt">
<l:task icon="images/24x24/up.gif" href="${it.upUrl}" title="${%Back to Project}" />
<l:task icon="images/24x24/search.gif" href="${buildUrl.baseUrl}/" title="${%Status}" />
<l:task icon="images/24x24/notepad.gif" href="${buildUrl.baseUrl}/changes" title="${%Changes}" />
<j:choose>
<j:when test="${it.logFile.length() > 200000}">
<!-- Show raw link directly so user need not click through live console page. -->
<div class="task">
<a href="${buildUrl.baseUrl}/console">
<img width="24" height="24" style="margin: 2px;" src="${imagesURL}/24x24/terminal.gif" alt=""/>
</a>
<st:nbsp />
<a href="${buildUrl.baseUrl}/console">${%Console Output}</a>
<st:nbsp />
<a href="${buildUrl.baseUrl}/consoleText">[${%raw}]</a>
</div>
</j:when>
<j:otherwise>
<l:task icon="images/24x24/terminal.gif" href="${buildUrl.baseUrl}/console" title="${%Console Output}" />
</j:otherwise>
</j:choose>
</j:jelly>
\ No newline at end of file
<!--
The MIT License
Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, Daniel Dyer, Seiji Sogabe
Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, Daniel Dyer, Seiji Sogabe, 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
......@@ -25,7 +25,7 @@ THE SOFTWARE.
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define"
xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form" xmlns:lo="hudson:linkout">
<l:layout title="${it.owner} test - ${it.displayName}">
<st:include it="${it.owner}" page="sidepanel.jelly" />
<st:include page="sidepanel.jelly" />
<l:main-panel>
<j:set var="st" value="${it.status}" />
<h1 class="${st.cssClass}">
......@@ -54,26 +54,32 @@ THE SOFTWARE.
</j:if>
<div style="text-align:right;">
<a href="history">
${%took(it.durationString)}
</a>
</div>
<d:taglib uri="hudson:linkout">
<d:bean name="out" className="hudson.util.HyperlinkingOutTag"/>
</d:taglib>
<t:editableDescription permission="${it.owner.BUILD}"/>
<table style="margin-top: 1em; margin-left:0em;">
<j:forEach var="action" items="${it.testActions}">
<st:include page="summary.jelly" from="${action}" optional="true" it="${action}" />
</j:forEach>
</table>
<h3>${%Error Message}</h3>
<pre><lo:out value="${it.errorDetails}"/></pre>
<pre>"${it.annotate(it.errorDetails)}</pre>
<h3>${%Stacktrace}</h3>
<pre><lo:out value="${it.errorStackTrace}"/></pre>
<pre><lo:out value="${it.annotate(it.errorStackTrace)}"/></pre>
<j:if test="${!empty(it.stdout)}">
<h3>${%Standard Output}</h3>
<pre><lo:out value="${it.stdout}"/></pre>
<pre>${it.annotate(it.stdout)}</pre>
</j:if>
<j:if test="${!empty(it.stderr)}">
<h3>${%Standard Error}</h3>
<pre><lo:out value="${it.stderr}"/></pre>
<pre>${it.annotate(it.stderr)}</pre>
</j:if>
</l:main-panel>
</l:layout>
......
<!--
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.
-->
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define"
xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form" xmlns:lo="hudson:linkout">
<table class="pane sortable" id="testresult">
<tr>
<td class="pane-header" style="width:10em">${%Build}</td>
<td class="pane-header" style="width:10em">${%Test Description}</td>
<td class="pane-header" style="width:5em">${%Test Duration}</td>
<td class="pane-header" style="width:5em">${%Test Result}</td>
</tr>
<tbody>
<j:forEach var="b" items="${it.owner.parent.builds}">
<j:set var="test" value="${it.getResultInBuild(b)}"/>
<j:if test="${test != null}">
<tr>
<td class="pane">
<a href="${app.rootUrl}${b.url}testReport${p.url}">${b.fullDisplayName}</a>
<st:nbsp/>
<j:forEach var="badge" items="${test.testActions}">
<st:include it="${badge}" page="badge.jelly" optional="true"/>
</j:forEach>
</td>
<td class="pane" style="text-align:left">${test.description}</td>
<td class="pane" style="text-align:left" data="${test.duration}">${test.durationString}</td>
<td class="pane">
<j:set var="pst" value="${test.status}" />
<span class="${pst.cssClass}">
${pst.message}
</span>
</td>
</tr>
</j:if>
</j:forEach>
</tbody>
</table>
</j:jelly>
<!-- this is loaded on demand in the failed test results summary -->
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define"
xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form" xmlns:lo="hudson:linkout">
<j:choose>
<j:when test="${it.errorDetails!=null}">
<h3>Error Details</h3>
<pre>${it.errorDetails}</pre>
</j:when>
<j:otherwise>
<j:if test="${it.errorStackTrace!=null}">
<h3>Stack Trace</h3>
<pre>${it.errorStackTrace}</pre>
</j:if>
</j:otherwise>
</j:choose>
</j:jelly>
......@@ -34,7 +34,13 @@ THE SOFTWARE.
<tbody>
<j:forEach var="p" items="${it.children}" varStatus="status">
<tr>
<td class="pane"><a href="${p.safeName}">${p.name}</a></td>
<td class="pane">
<a href="${p.safeName}"><span style='${p.previousResult==null?"font-weight:bold":""}'>${p.name}</span></a>
<st:nbsp/>
<j:forEach var="badge" items="${p.testActions}">
<st:include it="${badge}" page="badge.jelly" optional="true"/>
</j:forEach>
</td>
<td class="pane" style="width:6em" data="${p.duration}">${p.durationString}</td>
<td class="pane" style="width:6em">
<j:set var="pst" value="${p.status}" />
......
<!--
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.
-->
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define"
xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form" xmlns:lo="hudson:linkout">
<table class="pane sortable" id="testresult">
<tr>
<td class="pane-header">${%Build}</td>
<td class="pane-header">${%Description}</td>
<td class="pane-header" style="width:5em">${%Duration}</td>
<td class="pane-header" style="width:5em">${%Fail}</td>
<td class="pane-header" style="width:5em">${%Skip}</td>
<td class="pane-header" style="width:5em">${%Total}</td>
</tr>
<tbody>
<j:forEach var="b" items="${it.owner.parent.builds}">
<j:set var="p" value="${it.getResultInBuild(b)}"/>
<j:if test="${p != null}">
<tr>
<td class="pane">
<a href="${app.rootUrl}${b.url}testReport${p.url}">${b.fullDisplayName}</a>
<st:nbsp/>
<j:forEach var="badge" items="${p.testActions}">
<st:include it="${badge}" page="badge.jelly" optional="true"/>
</j:forEach>
</td>
<td class="pane" style="text-align:right">${p.description}</td>
<td class="pane" style="text-align:right" data="${p.duration}">${p.durationString}</td>
<td class="pane" style="text-align:right">${p.failCount}</td>
<td class="pane" style="text-align:right">${p.skipCount}</td>
<td class="pane" style="text-align:right">${p.totalCount}</td>
</tr>
</j:if>
</j:forEach>
</tbody>
</table>
</j:jelly>
<!--
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.
-->
<!-- Displays the chart that show how long builds are taking -->
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form" xmlns:i="jelly:fmt">
<l:layout title="History for ${it.testObject.displayName}">
<script type="text/javascript">
function setCount() {
document.getElementById("graph").src = "countGraph";
document.getElementById("graph").lazyMap = "countMap";
document.getElementById("duration-link").style.display = "";
document.getElementById("count-link").style.display = "none";
}
function setDuration() {
document.getElementById("graph").src = "durationGraph"
document.getElementById("graph").lazyMap = "durationMap"
document.getElementById("duration-link").style.display = "none";
document.getElementById("count-link").style.display = "";
}
</script>
<st:include from="${it.testObject}" it="${it.testObject}" page="sidepanel.jelly" />
<l:main-panel>
<H2>History for ${it.testObject.displayName}</H2>
<j:choose>
<j:when test="${it.testObject.previousResult != null}">
<div align="center">
<img id="graph" src="durationGraph" width="600" height="300" lazymap="durationMap" alt="[Duration graph]"/>
</div>
<div align="center">
show
<a id="count-link" href="#" onclick='javascript:setCount()'>count</a>
<a id="duration-link" href="#" onclick="javascript:setDuration()" style="display:none;">duration</a>
</div>
</j:when>
<j:otherwise>
${%More than 1 builds are needed for the chart.}
</j:otherwise>
</j:choose>
<st:include from="${it.testObject}" it="${it.testObject}" page="list.jelly" optional="true"/>
</l:main-panel>
</l:layout>
</j:jelly>
\ No newline at end of file
<!--
The MIT License
Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi
Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, 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
......@@ -22,8 +22,27 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
-->
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form">
<f:entry title="${%Test report XMLs}" description="${%description}" field="testResults">
<f:textbox />
</f:entry>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define"
xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form">
<f:entry title="${%Test report XMLs}" description="${%description}"
field="testResults">
<f:textbox />
</f:entry>
<j:invokeStatic var="testDataPublisherDescriptors"
className="hudson.tasks.junit.TestDataPublisher" method="all" />
<j:if test="${testDataPublisherDescriptors.size() > 0}">
<f:entry title="Additional test report features" field="testDataPublishers">
<j:set var="testDataPublisherInstances" value="${instance.testDataPublishers}"/>
<table>
<j:forEach var="tdpd" items="${testDataPublisherDescriptors}">
<f:optionalBlock name="${tdpd.jsonSafeClassName}" help="${tdpd.helpFile}"
title="${tdpd.displayName}" checked="${testDataPublisherInstances.get(tdpd)!=null}">
<j:set var="descriptor" value="${tdpd}" />
<j:set var="instance" value="${testDataPublisherInstances.get(tdpd)}" />
<st:include from="${tdpd}" page="${tdpd.configPage}" optional="true" />
</f:optionalBlock>
</j:forEach>
</table>
</f:entry>
</j:if>
</j:jelly>
\ No newline at end of file
<!--
The MIT License
Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, id:cactusman
Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, id:cactusman, 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
......@@ -23,24 +23,59 @@ THE SOFTWARE.
-->
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form">
<script type="text/javascript">
<!-- TODO make sure load doesn't happen every time -->
function showStackTrace(id,query) {
var element = document.getElementById(id)
element.style.display = "";
document.getElementById(id + "-showlink").style.display = "none";
document.getElementById(id + "-hidelink").style.display = "";
var rqo = new XMLHttpRequest();
rqo.open('GET', query, true);
rqo.onreadystatechange = function() { element.innerHTML = rqo.responseText; }
rqo.send(null);
}
function hideStackTrace(id) {
document.getElementById(id).style.display = "none";
document.getElementById(id + "-showlink").style.display = "";
document.getElementById(id + "-hidelink").style.display = "none";
}
</script>
<j:if test="${it.failCount!=0}">
<h2>${%All Failed Tests}</h2>
<table class="pane sortable">
<tr>
<td class="pane-header">${%Test Name}</td>
<td class="pane-header" style="width:4em">${%Duration}</td>
<td class="pane-header" style="width:4em">${%Age}</td>
<td class="pane-header" style="width:3em">${%Age}</td>
</tr>
<j:forEach var="f" items="${it.failedTests}" varStatus="i">
<tr>
<td class="pane">
<a id="test-${f.fullName}-showlink" href="#"
onclick='javascript:showStackTrace("test-${f.fullName}","${f.getRelativePathFrom(it)}/summary")'>&gt;&gt;&gt;</a>
<a id="test-${f.fullName}-hidelink" href="#" style="display:none"
onclick='javascript:hideStackTrace("test-${f.fullName}")'>&lt;&lt;&lt;</a>
<st:nbsp/>
<a href="${f.getRelativePathFrom(it)}"><st:out value="${f.fullName}"/></a>
<st:nbsp/>
<j:forEach var="badge" items="${f.testActions}">
<st:include it="${badge}" page="badge.jelly" optional="true"/>
</j:forEach>
<div id="test-${f.fullName}" class="hidden" style="display:none">
${%Loading...}
</div>
</td>
<td class="pane" style="text-align:right;">
${f.duration}
</td>
<td class="pane" style="text-align:right;">
${f.age}
<a href="${rootUrl}/${f.failedSinceRun.url}">${f.age}</a>
</td>
</tr>
</j:forEach>
......@@ -55,6 +90,8 @@ THE SOFTWARE.
<td class="pane-header" style="width:5em">${%Duration}</td>
<td class="pane-header" style="width:5em">${%Fail}</td>
<td class="pane-header" style="width:1em; font-size:smaller; white-space:nowrap;">(${%diff})</td>
<td class="pane-header" style="width:5em">${%Skip}</td>
<td class="pane-header" style="width:1em; font-size:smaller; white-space:nowrap;">(${%diff})</td>
<td class="pane-header" style="width:5em">${%Total}</td>
<td class="pane-header" style="width:1em; font-size:smaller; white-space:nowrap;">(${%diff})</td>
</tr>
......@@ -62,12 +99,22 @@ THE SOFTWARE.
<j:forEach var="p" items="${it.children}">
<j:set var="prev" value="${p.previousResult}" />
<tr>
<td class="pane"><a href="${p.safeName}/">${p.name}</a></td>
<td class="pane">
<a href="${p.safeName}/"><span style='${prev==null?"font-weight:bold":""}'>${p.name}</span></a>
<st:nbsp/>
<j:forEach var="badge" items="${p.testActions}">
<st:include it="${badge}" page="badge.jelly" optional="true"/>
</j:forEach>
</td>
<td class="pane" style="text-align:right" data="${p.duration}">${p.durationString}</td>
<td class="pane" style="text-align:right">${p.failCount}</td>
<td class="pane" style="text-align:right">
${h.getDiffString2(p.failCount-prev.failCount)}
</td>
<td class="pane" style="text-align:right">${p.skipCount}</td>
<td class="pane" style="text-align:right">
${h.getDiffString2(p.skipCount-prev.skipCount)}
</td>
<td class="pane" style="text-align:right">${p.totalCount}</td>
<td class="pane" style="text-align:right">
${h.getDiffString2(p.totalCount-prev.totalCount)}
......
<!--
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.
-->
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define"
xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form" xmlns:lo="hudson:linkout">
<table class="pane sortable" id="testresult">
<tr>
<td class="pane-header">${%Build}</td>
<td class="pane-header">${%Description}</td>
<td class="pane-header" style="width:5em">${%Duration}</td>
<td class="pane-header" style="width:5em">${%Fail}</td>
<td class="pane-header" style="width:5em">${%Skip}</td>
<td class="pane-header" style="width:5em">${%Total}</td>
</tr>
<tbody>
<j:forEach var="b" items="${it.owner.parent.builds}">
<j:set var="p" value="${it.getResultInBuild(b)}"/>
<j:if test="${p != null}">
<tr>
<td class="pane">
<a href="${app.rootUrl}${b.url}testReport${p.url}">${b.fullDisplayName}</a>
<st:nbsp/>
<j:forEach var="badge" items="${p.testActions}">
<st:include it="${badge}" page="badge.jelly" optional="true"/>
</j:forEach>
</td>
<td class="pane" style="text-align:right">${p.description}</td>
<td class="pane" style="text-align:right" data="${p.duration}">${p.durationString}</td>
<td class="pane" style="text-align:right">${p.failCount}</td>
<td class="pane" style="text-align:right">${p.skipCount}</td>
<td class="pane" style="text-align:right">${p.totalCount}</td>
</tr>
</j:if>
</j:forEach>
</tbody>
</table>
</j:jelly>
<!--
The MIT License
Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi
Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, 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
......@@ -24,7 +24,7 @@ THE SOFTWARE.
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form" xmlns:test="/lib/test">
<l:layout title="${it.owner} ${it.displayName}">
<st:include it="${it.owner}" page="sidepanel.jelly" />
<st:include page="sidepanel.jelly" />
<l:main-panel>
<h1>${it.title}</h1>
......@@ -32,6 +32,20 @@ THE SOFTWARE.
<test:bar />
<div style="text-align:right;">
<a href="history">
Took ${it.durationString}
</a>
</div>
<t:editableDescription permission="${it.owner.BUILD}"/>
<table style="margin-top: 1em; margin-left:0em;">
<j:forEach var="action" items="${it.testActions}">
<st:include page="summary.jelly" from="${action}" optional="true" it="${action}" />
</j:forEach>
</table>
<st:include page="body.jelly" />
</l:main-panel>
</l:layout>
......
<!--
The MIT License
Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, 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.
-->
<!--
Side panel for the build view.
-->
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form" xmlns:i="jelly:fmt">
<l:header />
<l:side-panel>
<l:tasks>
<j:set var="buildUrl" value="${h.decompose(request)}" />
<st:include it="${it.owner}" page="tasks.jelly"/>
<l:task icon="images/24x24/graph.gif" href="history" title="History"/>
<st:include it="${it.owner}" page="actions.jelly" />
<j:forEach var="action" items="${it.testActions}">
<j:if test="${action.iconFileName!=null}">
<l:task icon="${h.getIconFilePath(action)}" title="${action.displayName}"
href="${action.urlName}/" />
</j:if>
</j:forEach>
<j:if test="${it.owner.previousBuild!=null}">
<l:task icon="images/24x24/previous.gif" href="${buildUrl.previousBuildUrl}" title="${%Previous Build}" />
</j:if>
<j:if test="${it.owner.nextBuild!=null}">
<l:task icon="images/24x24/next.gif" href="${buildUrl.nextBuildUrl}" title="${%Next Build}" />
</j:if>
</l:tasks>
</l:side-panel>
</j:jelly>
\ No newline at end of file
<!--
The MIT License
Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, Red Hat, Inc.
Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, Red Hat, 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
......@@ -41,7 +41,7 @@ THE SOFTWARE.
<j:forEach var="report" items="${it.childReports}">
<tr>
<td class="pane">
<a href="../${report.child.project.shortUrl}">${report.child.project.name}</a>
<a href="../${report.child.project.shortUrl}/testReport">${report.child.project.name}</a>
</td>
<td class="pane" style="text-align:right;">
${report.result.durationString}
......@@ -73,7 +73,7 @@ THE SOFTWARE.
<h3>
<a name="${report.child.project.name}"/>
<a href="../${report.child.project.shortUrl}">${report.child.project.name}</a>
<a href="../${report.child.project.shortUrl}/testReport">${report.child.project.name}</a>
</h3>
<table class="pane sortable">
......@@ -87,6 +87,10 @@ THE SOFTWARE.
<td class="pane">
<a href="../${report.child.project.shortUrl}/testReport/${f.getRelativePathFrom(report.result)}">
<st:out value="${f.fullName}"/>
<st:nbsp/>
<j:forEach var="badge" items="${f.testActions}">
<st:include it="${badge}" page="badge.jelly" optional="true"/>
</j:forEach>
</a>
</td>
<td class="pane" style="text-align:right;">
......
/*
* 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.maven;
import hudson.Extension;
import hudson.Launcher;
import hudson.maven.reporters.SurefireReport;
import hudson.model.AbstractBuild;
import hudson.model.AbstractProject;
import hudson.model.BuildListener;
import hudson.model.Descriptor;
import hudson.model.Saveable;
import hudson.tasks.BuildStepDescriptor;
import hudson.tasks.BuildStepMonitor;
import hudson.tasks.Publisher;
import hudson.tasks.Recorder;
import hudson.tasks.junit.TestDataPublisher;
import hudson.tasks.junit.TestResultAction.Data;
import hudson.util.DescribableList;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import net.sf.json.JSONObject;
import org.kohsuke.stapler.StaplerRequest;
public class MavenTestDataPublisher extends Recorder {
private final DescribableList<TestDataPublisher, Descriptor<TestDataPublisher>> testDataPublishers;
public MavenTestDataPublisher(
DescribableList<TestDataPublisher, Descriptor<TestDataPublisher>> testDataPublishers) {
super();
this.testDataPublishers = testDataPublishers;
}
public BuildStepMonitor getRequiredMonitorService() {
return BuildStepMonitor.STEP;
}
public boolean perform(AbstractBuild build, Launcher launcher,
BuildListener listener) throws InterruptedException, IOException {
SurefireReport report = build.getAction(SurefireReport.class);
if (report == null) {
return true;
}
List<Data> data = new ArrayList<Data>();
if (testDataPublishers != null) {
for (TestDataPublisher tdp : testDataPublishers) {
Data d = tdp.getTestData(build, launcher, listener, report.getResult());
if (d != null) {
data.add(d);
}
}
}
report.setData(data);
return true;
}
public DescribableList<TestDataPublisher, Descriptor<TestDataPublisher>> getTestDataPublishers() {
return testDataPublishers;
}
@Extension
public static class DescriptorImpl extends BuildStepDescriptor<Publisher> {
@Override
public String getDisplayName() {
return "Additional test report features";
}
@Override
public boolean isApplicable(Class<? extends AbstractProject> jobType) {
return MavenModuleSet.class.isAssignableFrom(jobType) && !TestDataPublisher.all().isEmpty();
}
@Override
public Publisher newInstance(StaplerRequest req, JSONObject formData)
throws hudson.model.Descriptor.FormException {
DescribableList<TestDataPublisher, Descriptor<TestDataPublisher>> testDataPublishers = new DescribableList<TestDataPublisher, Descriptor<TestDataPublisher>>(
new Saveable() {
public void save() throws IOException {
// no-op
}
});
testDataPublishers.rebuild(req, formData, TestDataPublisher.all());
return new MavenTestDataPublisher(testDataPublishers);
}
}
}
<!--
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.
-->
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define"
xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form">
<j:invokeStatic var="testDataPublisherDescriptors"
className="hudson.tasks.junit.TestDataPublisher" method="all" />
<j:if test="${testDataPublisherDescriptors.size() > 0}">
<j:set var="testDataPublisherInstances" value="${instance.testDataPublishers}" />
<f:entry title="" field="testDataPublishers">
<table width="100%">
<j:forEach var="tdpd" items="${testDataPublisherDescriptors}">
<f:optionalBlock name="${tdpd.jsonSafeClassName}"
help="${tdpd.helpFile}" title="${tdpd.displayName}"
checked="${testDataPublisherInstances.get(tdpd)!=null}">
<j:set var="descriptor" value="${tdpd}" />
<j:set var="instance" value="${testDataPublisherInstances.get(tdpd)}" />
<st:include from="${tdpd}" page="${tdpd.configPage}"
optional="true" />
</f:optionalBlock>
</j:forEach>
</table>
</f:entry>
</j:if>
</j:jelly>
\ No newline at end of file
package hudson.tasks.junit;
import hudson.FilePath;
import hudson.Launcher;
import hudson.model.AbstractBuild;
import hudson.model.BuildListener;
import hudson.model.FreeStyleBuild;
import hudson.model.FreeStyleProject;
import hudson.model.Hudson;
import hudson.tasks.Builder;
import java.io.IOException;
import java.io.Serializable;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.List;
import java.util.concurrent.TimeUnit;
import junit.framework.Assert;
import org.jvnet.hudson.test.HudsonTestCase;
import org.jvnet.hudson.test.recipes.LocalData;
import org.xml.sax.SAXException;
import com.gargoylesoftware.htmlunit.html.HtmlForm;
import com.gargoylesoftware.htmlunit.html.HtmlPage;
public class JUnitResultArchiverTest extends HudsonTestCase {
public static final class TouchBuilder extends Builder implements Serializable {
@Override
public boolean perform(AbstractBuild<?, ?> build,
Launcher launcher, BuildListener listener)
throws InterruptedException, IOException {
for (FilePath f: build.getWorkspace().list()) {
f.touch(System.currentTimeMillis());
}
return true;
}
}
private FreeStyleProject project;
private JUnitResultArchiver archiver;
@Override
protected void setUp() throws Exception {
super.setUp();
project = createFreeStyleProject("junit");
archiver = new JUnitResultArchiver("*.xml");
project.getPublishersList().add(archiver);
project.getBuildersList().add(new TouchBuilder());
}
@LocalData
public void testBasic() throws Exception {
FreeStyleBuild build = project.scheduleBuild2(0).get(10, TimeUnit.SECONDS);
assertTestResults(build);
WebClient wc =new WebClient();
wc.getPage(project); // project page
wc.getPage(build); // build page
wc.getPage(build, "testReport"); // test report
wc.getPage(build, "testReport/hudson.security"); // package
wc.getPage(build, "testReport/hudson.security/HudsonPrivateSecurityRealmTest/"); // class
wc.getPage(build, "testReport/hudson.security/HudsonPrivateSecurityRealmTest/testDataCompatibilityWith1_282/"); // method
}
private void assertTestResults(FreeStyleBuild build) {
TestResultAction testResultAction = build.getAction(TestResultAction.class);
assertNotNull("no TestResultAction", testResultAction);
TestResult result = testResultAction.getResult();
assertNotNull("no TestResult", result);
assertEquals("should have 1 failing test", 1, testResultAction.getFailCount());
assertEquals("should have 1 failing test", 1, result.getFailCount());
assertEquals("should have 132 total tests", 132, testResultAction.getTotalCount());
assertEquals("should have 132 total tests", 132, result.getTotalCount());
}
@LocalData
public void testPersistence() throws Exception {
project.scheduleBuild2(0).get(10, TimeUnit.SECONDS);
reloadHudson();
FreeStyleBuild build = project.getBuildByNumber(1);
assertTestResults(build);
}
private void reloadHudson() throws NoSuchMethodException,
IllegalAccessException, InvocationTargetException {
Method m = Hudson.class.getDeclaredMethod("load");
m.setAccessible(true);
m.invoke(hudson);
project = (FreeStyleProject) hudson.getItem("junit");
}
@LocalData
public void testSetDescription() throws Exception {
FreeStyleBuild build = project.scheduleBuild2(0).get(10, TimeUnit.SECONDS);
CaseResult caseResult = build.getAction(TestResultAction.class).getFailedTests().get(0);
String url = build.getUrl() + "/testReport/" + caseResult.getRelativePathFrom(caseResult.getTestResult());
testSetDescription(url, caseResult);
ClassResult classResult = caseResult.getParent();
url = build.getUrl() + "/testReport/" + classResult.getParent().getSafeName() + "/" + classResult.getSafeName();
testSetDescription(url, classResult);
PackageResult packageResult = classResult.getParent();
url = build.getUrl() + "/testReport/" + classResult.getParent().getSafeName();
testSetDescription(url, packageResult);
}
private void testSetDescription(String url, TestObject object)
throws IOException, SAXException, Exception {
HtmlPage page = new WebClient().goTo(url);
page.getAnchorByHref("editDescription").click();
HtmlForm form = findForm(page, "submitDescription");
form.getTextAreaByName("description").setText("description");
submit(form);
assertEquals("description", object.getDescription());
}
private HtmlForm findForm(HtmlPage page, String action) {
for (HtmlForm form: page.getForms()) {
if (action.equals(form.getActionAttribute())) {
return form;
}
}
fail("no form found");
return null;
}
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册