/* * The MIT License * * Copyright (c) 2004-2010, Sun Microsystems, Inc., Kohsuke Kawaguchi, * Tom Huybrechts, Yahoo!, Inc. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package hudson.tasks.test; import hudson.Util; import hudson.Functions; import hudson.model.*; import hudson.tasks.junit.History; import hudson.tasks.junit.TestAction; import hudson.tasks.junit.TestResultAction; import org.kohsuke.stapler.*; import org.kohsuke.stapler.export.ExportedBean; import com.google.common.collect.MapMaker; import javax.servlet.ServletException; import java.io.IOException; import java.util.*; import java.util.logging.Logger; /** * Base class for all test result objects. * For compatibility with code that expects this class to be in hudson.tasks.junit, * we've created a pure-abstract class, hudson.tasks.junit.TestObject. That * stub class is deprecated; instead, people should use this class. * * @author Kohsuke Kawaguchi */ @ExportedBean public abstract class TestObject extends hudson.tasks.junit.TestObject { private static final Logger LOGGER = Logger.getLogger(TestObject.class.getName()); private volatile transient String id; public abstract AbstractBuild<?, ?> getOwner(); /** * Reverse pointer of {@link TabulatedResult#getChildren()}. */ public abstract TestObject getParent(); @Override public final String getId() { if (id == null) { StringBuilder buf = new StringBuilder(); buf.append(getSafeName()); TestObject parent = getParent(); if (parent != null) { String parentId = parent.getId(); if ((parentId != null) && (parentId.length() > 0)) { buf.insert(0, '/'); buf.insert(0, parent.getId()); } } id = buf.toString(); } return id; } /** * Returns url relative to TestResult */ @Override public String getUrl() { return '/' + getId(); } /** * Returns the top level test result data. * * @deprecated This method returns a JUnit specific class. Use * {@link #getTopLevelTestResult()} instead for a more general interface. * @return */ @Override public hudson.tasks.junit.TestResult getTestResult() { TestObject parent = getParent(); return (parent == null ? null : getParent().getTestResult()); } /** * Returns the top level test result data. * * @return */ public TestResult getTopLevelTestResult() { TestObject parent = getParent(); return (parent == null ? null : getParent().getTopLevelTestResult()); } /** * Computes the relative path to get to this test object from <code>it</code>. If * <code>it</code> does not appear in the parent chain for this object, a * relative path from the server root will be returned. * * @return A relative path to this object, potentially from the top of the * Hudson object model */ public String getRelativePathFrom(TestObject it) { // if (it is one of my ancestors) { // return a relative path from it // } else { // return a complete path starting with "/" // } if (it==this) { return "."; } StringBuilder buf = new StringBuilder(); TestObject next = this; TestObject cur = this; // Walk up my ancesotors from leaf to root, looking for "it" // and accumulating a relative url as I go while (next!=null && it!=next) { cur = next; buf.insert(0,'/'); buf.insert(0,cur.getSafeName()); next = cur.getParent(); } if (it==next) { return buf.toString(); } else { // Keep adding on to the string we've built so far // Start with the test result action AbstractTestResultAction action = getTestResultAction(); if (action==null) { LOGGER.warning("trying to get relative path, but we can't determine the action that owns this result."); return ""; // this won't take us to the right place, but it also won't 404. } buf.insert(0,'/'); buf.insert(0,action.getUrlName()); // Now the build AbstractBuild<?,?> myBuild = cur.getOwner(); if (myBuild ==null) { LOGGER.warning("trying to get relative path, but we can't determine the build that owns this result."); return ""; // this won't take us to the right place, but it also won't 404. } buf.insert(0,'/'); buf.insert(0,myBuild.getUrl()); // If we're inside a stapler request, just delegate to Hudson.Functions to get the relative path! StaplerRequest req = Stapler.getCurrentRequest(); if (req!=null && myBuild instanceof Item) { buf.insert(0, '/'); // Ugly but I don't see how else to convince the compiler that myBuild is an Item Item myBuildAsItem = (Item) myBuild; buf.insert(0, Functions.getRelativeLinkTo(myBuildAsItem)); } else { // We're not in a stapler request. Okay, give up. LOGGER.info("trying to get relative path, but it is not my ancestor, and we're not in a stapler request. Trying absolute hudson url..."); String hudsonRootUrl = Hudson.getInstance().getRootUrl(); if (hudsonRootUrl==null||hudsonRootUrl.length()==0) { LOGGER.warning("Can't find anything like a decent hudson url. Punting, returning empty string."); return ""; } buf.insert(0, '/'); buf.insert(0, hudsonRootUrl); } LOGGER.info("Here's our relative path: " + buf.toString()); return buf.toString(); } } /** * Subclasses may override this method if they are * associated with a particular subclass of * AbstractTestResultAction. * * @return the test result action that connects this test result to a particular build */ @Override public AbstractTestResultAction getTestResultAction() { AbstractBuild<?, ?> owner = getOwner(); if (owner != null) { return owner.getAction(AbstractTestResultAction.class); } else { LOGGER.warning("owner is null when trying to getTestResultAction."); return null; } } /** * Get a list of all TestActions associated with this TestObject. * @return */ @Override public List<TestAction> getTestActions() { AbstractTestResultAction atra = getTestResultAction(); if ((atra != null) && (atra instanceof TestResultAction)) { TestResultAction tra = (TestResultAction) atra; return tra.getActions(this); } else { return new ArrayList<TestAction>(); } } /** * Gets a test action of the class passed in. * @param klazz * @param <T> an instance of the class passed in * @return */ @Override public <T> T getTestAction(Class<T> klazz) { for (TestAction action : getTestActions()) { if (klazz.isAssignableFrom(action.getClass())) { return klazz.cast(action); } } return null; } /** * Gets the counterpart of this {@link TestResult} in the previous run. * * @return null if no such counter part exists. */ public abstract TestResult getPreviousResult(); /** * Gets the counterpart of this {@link TestResult} in the specified run. * * @return null if no such counter part exists. */ public abstract TestResult getResultInBuild(AbstractBuild<?, ?> build); /** * Find the test result corresponding to the one identified by <code>id></code> * withint this test result. * * @param id The path to the original test result * @return A corresponding test result, or null if there is no corresponding * result. */ public abstract TestResult findCorrespondingResult(String id); /** * 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. */ @Override public String getDurationString() { return Util.getTimeSpanString((long) (getDuration() * 1000)); } @Override public String getDescription() { AbstractTestResultAction action = getTestResultAction(); if (action != null) { return action.getDescription(this); } return ""; } @Override public void setDescription(String description) { AbstractTestResultAction action = getTestResultAction(); if (action != null) { action.setDescription(this, description); } } /** * Exposes this object through the remote API. */ @Override public Api getApi() { return new Api(this); } /** * Gets the name of this object. */ @Override public/* abstract */ String getName() { return ""; } /** * Gets the version of {@link #getName()} that's URL-safe. */ @Override public String getSafeName() { return safe(getName()); } @Override 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 MapMaker().weakKeys().makeMap(); /** * Replaces URL-unsafe characters. */ public static String safe(String s) { // 3 replace calls is still 2-3x faster than a regex replaceAll return s.replace('/', '_').replace('\\', '_').replace(':', '_'); } /** * 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. */ @Override public int getTotalCount() { return getPassCount() + getFailCount() + getSkipCount(); } @Override 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 { if (getOwner() == null) { LOGGER.severe("getOwner() is null, can't save description."); } else { getOwner().checkPermission(Run.UPDATE); setDescription(description); getOwner().save(); } return new HttpRedirect("."); } private static final long serialVersionUID = 1L; }