All Downloads are FREE. Search and download functionalities are using the official Maven repository.

hudson.tasks.junit.TestResult Maven / Gradle / Ivy

The newest version!
/*******************************************************************************
 *
 * 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 suites = new ArrayList();
    /**
     * {@link #suites} keyed by their names for faster lookup.
     */
    private transient Map suitesByName;
    /**
     * Results tabulated by package.
     */
    private transient Map 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 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, "", "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, "", 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 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 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 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/> * 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/> * 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 getChildren() { return byPackages.values(); } /** * Whether this test result has children. */ @Override public boolean hasChildren() { return !suites.isEmpty(); } @Exported(inline = true, visibility = 9) public Collection 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(); failedTests = new ArrayList(); byPackages = new TreeMap(); 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 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. * *

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(); totalTests = 0; failedTests = new ArrayList(); byPackages = new TreeMap(); } 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 }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy