/*******************************************************************************
*
* Copyright (c) 2004-2009 Oracle Corporation.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
*
* Kohsuke Kawaguchi, Daniel Dyer, id:cactusman, Tom Huybrechts, Yahoo!, Inc.
*
*
*******************************************************************************/
package hudson.tasks.junit;
import hudson.AbortException;
import hudson.Util;
import hudson.model.AbstractBuild;
import hudson.model.Run;
import hudson.tasks.test.MetaTabulatedResult;
import hudson.tasks.test.TestObject;
import hudson.tasks.test.AbstractTestResultAction;
import hudson.util.IOException2;
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;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.logging.Logger;
/**
* Root of all the test results for one build.
*
* @author Kohsuke Kawaguchi
*/
public final class TestResult extends MetaTabulatedResult {
private static final Logger LOGGER = Logger.getLogger(TestResult.class.getName());
/**
* List of all {@link SuiteResult}s in this test. This is the core data
* structure to be persisted in the disk.
*/
private final List<SuiteResult> suites = new ArrayList<SuiteResult>();
/**
* {@link #suites} keyed by their names for faster lookup.
*/
private transient Map<String, SuiteResult> suitesByName;
/**
* Results tabulated by package.
*/
private transient Map<String, PackageResult> byPackages;
// set during the freeze phase
private transient AbstractTestResultAction parentAction;
private transient TestObject parent;
/**
* Number of all tests.
*/
private transient int totalTests;
private transient int skippedTests;
private float duration;
/**
* Number of failed/error tests.
*/
private transient List<CaseResult> failedTests;
private final boolean keepLongStdio;
/**
* Creates an empty result.
*/
public TestResult() {
keepLongStdio = false;
}
@Deprecated
public TestResult(long buildTime, DirectoryScanner results) throws IOException {
this(buildTime, results, false);
}
/**
* Collect reports from the given {@link DirectoryScanner}, while filtering
* out all files that were created before the given time.
*
* @param keepLongStdio if true, retain a suite's complete stdout/stderr
* even if this is huge and the suite passed
* @since 1.358
*/
public TestResult(long buildTime, DirectoryScanner results, boolean keepLongStdio) throws IOException {
this.keepLongStdio = keepLongStdio;
parse(buildTime, results);
}
public TestObject getParent() {
return parent;
}
@Override
public void setParent(TestObject parent) {
this.parent = parent;
}
@Override
public TestResult getTestResult() {
return this;
}
/**
* Collect reports from the given {@link DirectoryScanner}, while filtering
* out all files that were created before the given time.
*/
public void parse(long buildTime, DirectoryScanner results) throws IOException {
String[] includedFiles = results.getIncludedFiles();
File baseDir = results.getBasedir();
boolean parsed = false;
for (String value : includedFiles) {
File reportFile = new File(baseDir, value);
// only count files that were actually updated during this build
if ((buildTime - 3000/*error margin*/ <= reportFile.lastModified()) || !checkTimestamps) {
if (reportFile.length() == 0) {
// this is a typical problem when JVM quits abnormally, like OutOfMemoryError during a test.
SuiteResult sr = new SuiteResult(reportFile.getName(), "", "");
sr.addCase(new CaseResult(sr, "<init>", "Test report file " + reportFile.getAbsolutePath() + " was length 0"));
add(sr);
} else {
parse(reportFile);
}
parsed = true;
}
}
if (!parsed) {
long localTime = System.currentTimeMillis();
if (localTime < buildTime - 1000) /*margin*/ // build time is in the the future. clock on this slave must be running behind
{
throw new AbortException(
"Clock on this slave is out of sync with the master, and therefore \n"
+ "I can't figure out what test results are new and what are old.\n"
+ "Please keep the slave clock in sync with the master.");
}
File f = new File(baseDir, includedFiles[0]);
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", f,
Util.getTimeSpanString(buildTime - f.lastModified())));
}
}
private void add(SuiteResult sr) {
for (SuiteResult s : suites) {
// a common problem is that people parse TEST-*.xml as well as TESTS-TestSuite.xml
// see http://www.nabble.com/Problem-with-duplicate-build-execution-td17549182.html for discussion
if (s.getName().equals(sr.getName()) && eq(s.getTimestamp(), sr.getTimestamp())) {
return; // duplicate
}
}
suites.add(sr);
duration += sr.getDuration();
}
private boolean eq(Object lhs, Object rhs) {
return lhs != null && rhs != null && lhs.equals(rhs);
}
/**
* Parses an additional report file.
*/
public void parse(File reportFile) throws IOException {
try {
for (SuiteResult suiteResult : SuiteResult.parse(reportFile, keepLongStdio)) {
add(suiteResult);
}
} catch (RuntimeException e) {
throw new IOException2("Failed to read " + reportFile, e);
} catch (DocumentException e) {
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 {
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);
}
}
}
public String getDisplayName() {
return Messages.TestResult_getDisplayName();
}
@Override
public AbstractBuild<?, ?> getOwner() {
return (parentAction == null ? null : parentAction.owner);
}
@Override
public hudson.tasks.test.TestResult findCorrespondingResult(String id) {
if (getId().equals(id) || (id == null)) {
return this;
}
String firstElement = null;
String subId = null;
int sepIndex = id.indexOf('/');
if (sepIndex < 0) {
firstElement = id;
subId = null;
} else {
firstElement = id.substring(0, sepIndex);
subId = id.substring(sepIndex + 1);
if (subId.length() == 0) {
subId = null;
}
}
String packageName = null;
if (firstElement.equals(getId())) {
sepIndex = subId.indexOf('/');
if (sepIndex < 0) {
packageName = subId;
subId = null;
} else {
packageName = subId.substring(0, sepIndex);
subId = subId.substring(sepIndex + 1);
}
} else {
packageName = firstElement;
subId = null;
}
PackageResult child = byPackage(packageName);
if (child != null) {
if (subId != null) {
return child.findCorrespondingResult(subId);
} else {
return child;
}
} else {
return null;
}
}
@Override
public String getTitle() {
return Messages.TestResult_getTitle();
}
@Override
public String getChildTitle() {
return Messages.TestResult_getChildTitle();
}
@Exported(visibility = 999)
@Override
public float getDuration() {
return duration;
}
@Exported(visibility = 999)
@Override
public int getPassCount() {
return totalTests - getFailCount() - getSkipCount();
}
@Exported(visibility = 999)
@Override
public int getFailCount() {
if (failedTests == null) {
return 0;
} else {
return failedTests.size();
}
}
@Exported(visibility = 999)
@Override
public int getSkipCount() {
return skippedTests;
}
@Override
public List<CaseResult> getFailedTests() {
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() {
throw new UnsupportedOperationException(); // TODO: implement!(FIXME: generated)
}
/**
* 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 hudson.tasks.test.TestResult> getSkippedTests() {
throw new UnsupportedOperationException(); // TODO: implement!(FIXME: generated)
}
/**
* If this test failed, then return the build number when this test started
* failing.
*/
@Override
public int getFailedSince() {
throw new UnsupportedOperationException(); // TODO: implement!(FIXME: generated)
}
/**
* If this test failed, then return the run when this test started failing.
*/
@Override
public Run<?, ?> getFailedSinceRun() {
throw new UnsupportedOperationException(); // TODO: implement!(FIXME: generated)
}
/**
* The stdout of this test.
* <p/>
* <
* p/>
* Depending on the tool that produced the XML report, this method works
* somewhat inconsistently. With some tools (such as Maven surefire plugin),
* you get the accurate information, that is the stdout from this test case.
* With some other tools (such as the JUnit task in Ant), this method
* returns the stdout produced by the entire test suite.
* <p/>
* <
* p/>
* If you need to know which is the case, compare this output from
* {@link SuiteResult#getStdout()}.
*
* @since 1.294
*/
@Override
public String getStdout() {
StringBuilder sb = new StringBuilder();
for (SuiteResult suite : suites) {
sb.append("Standard Out (stdout) for Suite: " + suite.getName());
sb.append(suite.getStdout());
}
return sb.toString();
}
/**
* The stderr of this test.
*
* @see #getStdout()
* @since 1.294
*/
@Override
public String getStderr() {
StringBuilder sb = new StringBuilder();
for (SuiteResult suite : suites) {
sb.append("Standard Error (stderr) for Suite: " + suite.getName());
sb.append(suite.getStderr());
}
return sb.toString();
}
/**
* If there was an error or a failure, this is the stack trace, or otherwise
* null.
*/
@Override
public String getErrorStackTrace() {
return "No error stack traces available at this level. Drill down to individual tests to find stack traces.";
}
/**
* If there was an error or a failure, this is the text from the message.
*/
@Override
public String getErrorDetails() {
return "No error details available at this level. Drill down to individual tests to find details.";
}
/**
* @return true if the test was not skipped and did not fail, false
* otherwise.
*/
@Override
public boolean isPassed() {
return (getFailCount() == 0);
}
@Override
public Collection<PackageResult> getChildren() {
return byPackages.values();
}
/**
* Whether this test result has children.
*/
@Override
public boolean hasChildren() {
return !suites.isEmpty();
}
@Exported(inline = true, visibility = 9)
public Collection<SuiteResult> getSuites() {
return suites;
}
@Override
public String getName() {
return "junit";
}
@Override
public Object getDynamic(String token, StaplerRequest req, StaplerResponse rsp) {
if (token.equals(getId())) {
return this;
}
PackageResult result = byPackage(token);
if (result != null) {
return result;
} else {
return super.getDynamic(token, req, rsp);
}
}
public PackageResult byPackage(String packageName) {
return byPackages.get(packageName);
}
public SuiteResult getSuite(String name) {
return suitesByName.get(name);
}
@Override
public void setParentAction(AbstractTestResultAction action) {
this.parentAction = action;
tally(); // I want to be sure to inform our children when we get an action.
}
@Override
public AbstractTestResultAction getParentAction() {
return this.parentAction;
}
/**
* Recount my children.
*/
@Override
public void tally() {
/// Empty out data structures
// TODO: free children? memmory leak?
suitesByName = new HashMap<String, SuiteResult>();
failedTests = new ArrayList<CaseResult>();
byPackages = new TreeMap<String, PackageResult>();
totalTests = 0;
skippedTests = 0;
// Ask all of our children to tally themselves
for (SuiteResult s : suites) {
s.setParent(this); // kluge to prevent double-counting the results
suitesByName.put(s.getName(), s);
List<CaseResult> cases = s.getCases();
for (CaseResult cr : cases) {
cr.setParentAction(this.parentAction);
cr.setParentSuiteResult(s);
cr.tally();
String pkg = cr.getPackageName(), spkg = safe(pkg);
PackageResult pr = byPackage(spkg);
if (pr == null) {
byPackages.put(spkg, pr = new PackageResult(this, pkg));
}
pr.add(cr);
}
}
for (PackageResult pr : byPackages.values()) {
pr.tally();
skippedTests += pr.getSkipCount();
failedTests.addAll(pr.getFailedTests());
totalTests += pr.getTotalCount();
}
}
/**
* Builds up the transient part of the data structure from results
* {@link #parse(File) parsed} so far.
*
* <p> After the data is frozen, more files can be parsed and then freeze
* can be called again.
*/
public void freeze(TestResultAction parent) {
this.parentAction = parent;
if (suitesByName == null) {
// freeze for the first time
suitesByName = new HashMap<String, SuiteResult>();
totalTests = 0;
failedTests = new ArrayList<CaseResult>();
byPackages = new TreeMap<String, PackageResult>();
}
for (SuiteResult s : suites) {
if (!s.freeze(this)) // this is disturbing: has-a-parent is conflated with has-been-counted
{
continue;
}
suitesByName.put(s.getName(), s);
totalTests += s.getCases().size();
for (CaseResult cr : s.getCases()) {
if (cr.isSkipped()) {
skippedTests++;
} else if (!cr.isPassed()) {
failedTests.add(cr);
}
String pkg = cr.getPackageName(), spkg = safe(pkg);
PackageResult pr = byPackage(spkg);
if (pr == null) {
byPackages.put(spkg, pr = new PackageResult(this, pkg));
}
pr.add(cr);
}
}
Collections.sort(failedTests, CaseResult.BY_AGE);
for (PackageResult pr : byPackages.values()) {
pr.freeze();
}
}
private static final long serialVersionUID = 1L;
private static final boolean checkTimestamps = true; // TODO: change to System.getProperty
}