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

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

The newest version!
/*
 * The MIT License
 * 
 * Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, Daniel Dyer, Seiji Sogabe, Tom Huybrechts, Yahoo!, Inc.
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package hudson.tasks.junit;

import org.jvnet.localizer.Localizable;
import hudson.model.AbstractBuild;
import hudson.model.Run;
import hudson.tasks.test.TestResult;
import org.dom4j.Element;
import org.kohsuke.stapler.export.Exported;

import java.text.DecimalFormat;
import java.text.ParseException;
import java.util.*;
import java.util.logging.Logger;

import static java.util.Collections.emptyList;

/**
 * One test result.
 *
 * @author Kohsuke Kawaguchi
 */
public final class CaseResult extends TestResult implements Comparable {
    private static final Logger LOGGER = Logger.getLogger(CaseResult.class.getName());
    private final float duration;
    /**
     * In JUnit, a test is a method of a class. This field holds the fully qualified class name
     * that the test was in.
     */
    private final String className;
    /**
     * This field retains the method name.
     */
    private final String testName;
    private final boolean skipped;
    private final String errorStackTrace;
    private final String errorDetails;
    private transient SuiteResult parent;

    private transient ClassResult classResult;

    /**
     * Some tools report stdout and stderr at testcase level (such as Maven surefire plugin), others do so at
     * the suite level (such as Ant JUnit task.)
     *
     * If these information are reported at the test case level, these fields are set,
     * otherwise null, in which case {@link SuiteResult#stdout}.
     */
    private final String stdout,stderr;

    /**
     * This test has been failing since this build number (not id.)
     *
     * If {@link #isPassed() passing}, this field is left unused to 0.
     */
    private /*final*/ int failedSince;

    private static float parseTime(Element testCase) {
        String time = testCase.attributeValue("time");
        if(time!=null) {
            time = time.replace(",","");
            try {
                return Float.parseFloat(time);
            } catch (NumberFormatException e) {
                try {
                    return new DecimalFormat().parse(time).floatValue();
                } catch (ParseException x) {
                    // hmm, don't know what this format is.
                }
            }
        }
        return 0.0f;
    }

    CaseResult(SuiteResult parent, Element testCase, String testClassName, boolean keepLongStdio) {
        // schema for JUnit report XML format is not available in Ant,
        // so I don't know for sure what means what.
        // reports in http://www.nabble.com/difference-in-junit-publisher-and-ant-junitreport-tf4308604.html#a12265700
        // indicates that maybe I shouldn't use @classname altogether.

        //String cn = testCase.attributeValue("classname");
        //if(cn==null)
        //    // Maven seems to skip classname, and that shows up in testSuite/@name
        //    cn = parent.getName();


        /*
            According to http://www.nabble.com/NPE-(Fatal%3A-Null)-in-recording-junit-test-results-td23562964.html
            there's some odd-ball cases where testClassName is null but
            @name contains fully qualified name.
         */
        String nameAttr = testCase.attributeValue("name");
        if(testClassName==null && nameAttr.contains(".")) {
            testClassName = nameAttr.substring(0,nameAttr.lastIndexOf('.'));
            nameAttr = nameAttr.substring(nameAttr.lastIndexOf('.')+1);
        }

        className = testClassName;
        testName = nameAttr;
        errorStackTrace = getError(testCase);
        errorDetails = getErrorMessage(testCase);
        this.parent = parent;
        duration = parseTime(testCase);
        skipped = isMarkedAsSkipped(testCase);
        @SuppressWarnings("LeakingThisInConstructor")
        Collection _this = Collections.singleton(this);
        stdout = possiblyTrimStdio(_this, keepLongStdio, testCase.elementText("system-out"));
        stderr = possiblyTrimStdio(_this, keepLongStdio, testCase.elementText("system-err"));
    }

    private static final int HALF_MAX_SIZE = 500;
    static String possiblyTrimStdio(Collection results, boolean keepLongStdio, String stdio) { // HUDSON-6516
        if (stdio == null) {
            return null;
        }
        if (keepLongStdio) {
            return stdio;
        }
        for (CaseResult result : results) {
            if (result.errorStackTrace != null) {
                return stdio;
            }
        }
        int len = stdio.length();
        int middle = len - HALF_MAX_SIZE * 2;
        if (middle <= 0) {
            return stdio;
        }
        return stdio.substring(0, HALF_MAX_SIZE) + "...[truncated " + middle + " chars]..." + stdio.substring(len - HALF_MAX_SIZE, len);
    }

    /**
     * Used to create a fake failure, when Hudson fails to load data from XML files.
     */
    CaseResult(SuiteResult parent, String testName, String errorStackTrace) {
        this.className = parent == null ? "unnamed" : parent.getName();
        this.testName = testName;
        this.errorStackTrace = errorStackTrace;
        this.errorDetails = "";
        this.parent = parent;
        this.stdout = null;
        this.stderr = null;
        this.duration = 0.0f;
        this.skipped = false;
    }
    
    public ClassResult getParent() {
    	return classResult;
    }

    private static String getError(Element testCase) {
        String msg = testCase.elementText("error");
        if(msg!=null)
            return msg;
        return testCase.elementText("failure");
    }

    private static String getErrorMessage(Element testCase) {

        Element msg = testCase.element("error");
        if (msg == null) {
            msg = testCase.element("failure");
        }
        if (msg == null) {
            return null; // no error or failure elements! damn!
        }

        return msg.attributeValue("message");
    }

    /**
     * If the testCase element includes the skipped element (as output by TestNG), then
     * the test has neither passed nor failed, it was never run.
     */
    private static boolean isMarkedAsSkipped(Element testCase) {
        return testCase.element("skipped") != null;
    }

    public String getDisplayName() {
        return testName;
    }

    /**
     * Gets the name of the test, which is returned from {@code TestCase.getName()}
     *
     * 

* Note that this may contain any URL-unfriendly character. */ @Exported(visibility=999) public @Override String getName() { return testName; } /** * Gets the human readable title of this result object. */ @Override public String getTitle() { return "Case Result: " + getName(); } /** * Gets the duration of the test, in seconds */ @Exported(visibility=9) public float getDuration() { return duration; } /** * Gets the version of {@link #getName()} that's URL-safe. */ public @Override String getSafeName() { StringBuilder buf = new StringBuilder(testName); for( int i=0; i siblings = (classResult ==null ? Collections.emptyList(): classResult.getChildren()); return uniquifyName(siblings, buf.toString()); } /** * Gets the class name of a test class. */ @Exported(visibility=9) public String getClassName() { return className; } /** * Gets the simple (not qualified) class name. */ public String getSimpleName() { int idx = className.lastIndexOf('.'); return className.substring(idx+1); } /** * Gets the package name of a test case */ public String getPackageName() { int idx = className.lastIndexOf('.'); if(idx<0) return "(root)"; else return className.substring(0,idx); } public String getFullName() { 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. */ @Exported(visibility=9) public int getFailedSince() { // If we haven't calculated failedSince yet, and we should, // do it now. if (failedSince==0 && getFailCount()==1) { CaseResult prev = getPreviousResult(); if(prev!=null && !prev.isPassed()) this.failedSince = prev.failedSince; else if (getOwner() != null) { this.failedSince = getOwner().getNumber(); } else { LOGGER.warning("trouble calculating getFailedSince. We've got prev, but no owner."); // failedSince will be 0, which isn't correct. } } return failedSince; } public Run getFailedSinceRun() { return getOwner().getParent().getBuildByNumber(getFailedSince()); } /** * Gets the number of consecutive builds (including this) * that this test case has been failing. */ @Exported(visibility=9) public int getAge() { if(isPassed()) return 0; else if (getOwner() != null) { return getOwner().getNumber()-getFailedSince()+1; } else { LOGGER.fine("Trying to get age of a CaseResult without an owner"); return 0; } } /** * The stdout of this test. * *

* 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. * *

* If you need to know which is the case, compare this output from {@link SuiteResult#getStdout()}. * @since 1.294 */ @Exported public String getStdout() { if(stdout!=null) return stdout; SuiteResult sr = getSuiteResult(); if (sr==null) return ""; return getSuiteResult().getStdout(); } /** * The stderr of this test. * * @see #getStdout() * @since 1.294 */ @Exported public String getStderr() { if(stderr!=null) return stderr; SuiteResult sr = getSuiteResult(); if (sr==null) return ""; return getSuiteResult().getStderr(); } @Override public CaseResult getPreviousResult() { if (parent == null) return null; SuiteResult pr = parent.getPreviousResult(); if(pr==null) return null; return pr.getCase(getName()); } /** * Case results have no children * @return null */ @Override public TestResult findCorrespondingResult(String id) { if (id.equals(safe(getName()))) { return this; } return null; } /** * Gets the "children" of this test result that failed * * @return the children of this test result, if any, or an empty collection */ @Override public Collection getFailedTests() { return singletonListOrEmpty(!isPassed()); } /** * 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() { return singletonListOrEmpty(isPassed()); } /** * 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() { return singletonListOrEmpty(isSkipped()); } private Collection singletonListOrEmpty(boolean f) { if (f) return Collections.singletonList(this); else return emptyList(); } /** * If there was an error or a failure, this is the stack trace, or otherwise null. */ @Exported public String getErrorStackTrace() { return errorStackTrace; } /** * If there was an error or a failure, this is the text from the message. */ @Exported public String getErrorDetails() { return errorDetails; } /** * @return true if the test was not skipped and did not fail, false otherwise. */ public boolean isPassed() { return !skipped && errorStackTrace==null; } /** * Tests whether the test was skipped or not. TestNG allows tests to be * skipped if their dependencies fail or they are part of a group that has * been configured to be skipped. * @return true if the test was not executed, false otherwise. */ @Exported(visibility=9) public boolean isSkipped() { return skipped; } public SuiteResult getSuiteResult() { return parent; } @Override public AbstractBuild getOwner() { SuiteResult sr = getSuiteResult(); if (sr==null) { LOGGER.warning("In getOwner(), getSuiteResult is null"); return null; } hudson.tasks.junit.TestResult tr = sr.getParent(); if (tr==null) { LOGGER.warning("In getOwner(), suiteResult.getParent() is null."); return null; } return tr.getOwner(); } public void setParentSuiteResult(SuiteResult parent) { this.parent = parent; } public void freeze(SuiteResult parent) { this.parent = parent; // some old test data doesn't have failedSince value set, so for those compute them. if(!isPassed() && failedSince==0) { CaseResult prev = getPreviousResult(); if(prev!=null && !prev.isPassed()) this.failedSince = prev.failedSince; else this.failedSince = getOwner().getNumber(); } } public int compareTo(CaseResult that) { return this.getFullName().compareTo(that.getFullName()); } @Exported(name="status",visibility=9) // because stapler notices suffix 's' and remove it public Status getStatus() { if (skipped) { return Status.SKIPPED; } CaseResult pr = getPreviousResult(); if(pr==null) { return isPassed() ? Status.PASSED : Status.FAILED; } if(pr.isPassed()) { return isPassed() ? Status.PASSED : Status.REGRESSION; } else { return isPassed() ? Status.FIXED : Status.FAILED; } } /*package*/ void setClass(ClassResult classResult) { this.classResult = classResult; } /** * Constants that represent the status of this test. */ public enum Status { /** * This test runs OK, just like its previous run. */ PASSED("result-passed",Messages._CaseResult_Status_Passed(),true), /** * This test was skipped due to configuration or the * failure or skipping of a method that it depends on. */ SKIPPED("result-skipped",Messages._CaseResult_Status_Skipped(),false), /** * This test failed, just like its previous run. */ FAILED("result-failed",Messages._CaseResult_Status_Failed(),false), /** * This test has been failing, but now it runs OK. */ FIXED("result-fixed",Messages._CaseResult_Status_Fixed(),true), /** * This test has been running OK, but now it failed. */ REGRESSION("result-regression",Messages._CaseResult_Status_Regression(),false); private final String cssClass; private final Localizable message; public final boolean isOK; Status(String cssClass, Localizable message, boolean OK) { this.cssClass = cssClass; this.message = message; isOK = OK; } public String getCssClass() { return cssClass; } public String getMessage() { return message.toString(); } public boolean isRegression() { return this==REGRESSION; } } /** * For sorting errors by age. */ /*package*/ static final Comparator BY_AGE = new Comparator() { public int compare(CaseResult lhs, CaseResult rhs) { return lhs.getAge()-rhs.getAge(); } }; private static final long serialVersionUID = 1L; }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy