/*******************************************************************************
*
* Copyright (c) 2004-2010 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, Tom Huybrechts, Yahoo!, Inc.
*
*
*******************************************************************************/
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 String uniquifyName(Collection<? extends TestObject> siblings, String base) {
synchronized (UNIQUIFIED_NAMES) {
String uniquified = base;
Set<TestObject> siblingsPrevUsed = UNIQUIFIED_NAMES.get(base);
if (siblingsPrevUsed == null) {
siblingsPrevUsed = Collections.newSetFromMap(new WeakHashMap<TestObject, Boolean>());
UNIQUIFIED_NAMES.put(base, siblingsPrevUsed);
} else {
Set<TestObject> tmpFilter = new HashSet<TestObject>(siblingsPrevUsed);
tmpFilter.retainAll(new HashSet<TestObject>(siblings));
if (!tmpFilter.isEmpty()) {
uniquified = base + '_' + (tmpFilter.size() + 1);
}
}
siblingsPrevUsed.add(this);
return uniquified;
}
}
private static final Map<String, Set<TestObject>> 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;
}