package hudson.plugins.testng.results;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import hudson.model.Run;
import hudson.plugins.testng.util.FormatUtil;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import org.kohsuke.stapler.bind.JavaScriptMethod;
import org.kohsuke.stapler.export.Exported;
/**
* Handles package level results
*/
@SuppressFBWarnings(value="SE_NO_SERIALVERSIONID", justification="XStream does not care")
@SuppressWarnings("serial")
public class PackageResult extends BaseResult {
public static final String NO_PKG_NAME = "No Package";
//TODO: switch to using a Map instead of List
//list of all classes run from this package
private List<ClassResult> classList = new ArrayList<ClassResult>();
//cached
private List<MethodResult> sortedTestMethodsByStartTime = null;
//cached vars updated using tally method
private transient long startTime;
private transient long duration;
private transient int fail;
private transient int skip;
private transient int pass;
// Maximum size of methods in the method execution list
public static final int MAX_EXEC_MTHD_LIST_SIZE = 25;
public PackageResult(String name) {
super(name);
}
@Override
public void setRun(Run<?, ?> run) {
super.setRun(run);
for (ClassResult _class : classList) {
_class.setRun(run);
}
}
/**
* Can't change this to return seconds as expected by {@link hudson.tasks.test.TestObject} because
* it has already been exported
*
* @return duration in milliseconds
*/
@Exported
@Override
public float getDuration() {
return duration / 1000f;
}
public long getStartTime() {
return startTime;
}
public long getEndTime() {
return startTime + duration;
}
@Override
@Exported(visibility = 9, name = "fail")
public int getFailCount() {
return fail;
}
@Override
@Exported(visibility = 9, name = "skip")
public int getSkipCount() {
return skip;
}
@Override
@Exported(visibility = 9)
public int getTotalCount() {
return super.getTotalCount();
}
@Override
public int getPassCount() {
return pass;
}
/**
* Gets all the method results related to this package sorted by the time
* the methods were executed
*
* @return
*/
public List<MethodResult> getSortedTestMethodsByStartTime() {
if (sortedTestMethodsByStartTime == null) {
sortTestMethods();
}
return sortedTestMethodsByStartTime;
}
/**
* Gets table row representation for all the method results associated with
* this package (sorted based on start time)
*
* @return
*/
@JavaScriptMethod
public String getAllSortedTestMethodsByStartTime() {
return getMethodExecutionTableContent(getSortedTestMethodsByStartTime());
}
/**
* Gets table row representation for the first {@link #MAX_EXEC_MTHD_LIST_SIZE}
* method results associated with this package (sorted based on start time)
*
* @return
*/
@JavaScriptMethod
public String getFirstXSortedTestMethodsByStartTime() {
//returning the first MAX results only
List<MethodResult> list = getSortedTestMethodsByStartTime();
list = list.subList(0, list.size() > MAX_EXEC_MTHD_LIST_SIZE
? MAX_EXEC_MTHD_LIST_SIZE : list.size());
return getMethodExecutionTableContent(list);
}
/**
* Gets the table row representation for the specified method results
*
* @param mrList list of method result objects
* @return table row representation
*/
private String getMethodExecutionTableContent(List<MethodResult> mrList) {
StringBuilder sb = new StringBuilder(mrList.size() * 100);
for (MethodResult mr : mrList) {
sb.append("<tr><td align=\"left\">");
sb.append("<a href=\"").append(mr.getUpUrl()).append("\">");
sb.append(mr.getParent().getName()).append(".").append(mr.getName());
sb.append("</a>");
sb.append("</td><td align=\"center\">");
sb.append(FormatUtil.formatTime(mr.getDuration()));
sb.append("</td><td align=\"center\">");
sb.append(mr.getStartedAt());
sb.append("</td><td align=\"center\"><span class=\"").append(mr.getCssClass()).append("\">");
sb.append(mr.getStatus());
sb.append("</span></td></tr>");
}
return sb.toString();
}
@Override
public void tally() {
fail = 0;
skip = 0;
pass = 0;
List<long[]> timeSeries = new ArrayList<long[]>(classList.size());
for (ClassResult _c : classList) {
_c.setParent(this);
_c.tally();
fail += _c.getFailCount();
skip += _c.getSkipCount();
pass += _c.getPassCount();
timeSeries.add(new long[] {_c.getStartTime(), _c.getEndTime() - _c.getStartTime()});
}
Collections.sort(timeSeries, new Comparator<long[]>() {
public int compare(long[] ts1, long[] ts2) {
return ts1[0] < ts2[0] ? -1 : (ts1[0] > ts2[0] ? 1 : 0);
}
});
timeSeries.add(new long[] {System.currentTimeMillis(), 0}); //to help with following algorithm
startTime = timeSeries.get(0)[0]; //start time for all classes within this package
duration = 0;
int activeTS = 0;
int nextTS = 1;
do {
long[] ts1 = timeSeries.get(activeTS);
long[] ts2 = timeSeries.get(nextTS);
long s1 = ts1[0];
long e1 = ts1[0] + ts1[1];
long s2 = ts2[0];
long e2 = ts2[0] + ts2[1];
if (s1 <= s2 && e1 >= e2) {
//ts2 series is completely contained in ts1, so nothing to do
nextTS++;
continue;
}
if (e1 <= s2) {
//no overlap (disjoint time series)
duration += ts1[1];
} else {
//overlap
duration += s2 - s1;
}
activeTS = nextTS;
nextTS++;
} while (nextTS < timeSeries.size()); // we never process the last entry in the array
}
/**
* Sorts the test method results associated with this package based on the
* start time for method execution
*/
public void sortTestMethods() {
this.sortedTestMethodsByStartTime = new ArrayList<MethodResult>();
//for each class
Map<Date, List<MethodResult>> map = new HashMap<Date, List<MethodResult>>();
for (ClassResult aClass : classList) {
if (aClass.getTestMethods() != null) {
for (MethodResult aMethod : aClass.getTestMethods()) {
Date startDate = aMethod.getStartedAt();
if (!aMethod.getStatus().equalsIgnoreCase("skip")
&& startDate != null) {
if (map.containsKey(startDate)) {
map.get(startDate).add(aMethod);
} else {
List<MethodResult> list = new ArrayList<MethodResult>();
list.add(aMethod);
map.put(startDate, list);
}
}
}
}
}
List<Date> keys = new ArrayList<Date>(map.keySet());
Collections.sort(keys);
//now create the list with the order
for (Date key : keys) {
if (map.containsKey(key)) {
this.sortedTestMethodsByStartTime.addAll(map.get(key));
}
}
}
@Override
@Exported(name = "classs") // because stapler notices suffix 's' and remove it
public List<ClassResult> getChildren() {
return classList;
}
@Override
public boolean hasChildren() {
return classList != null && !classList.isEmpty();
}
/**
* {@inheritDoc}
*
* Overriding so that we can be backward compatible with shared links. We changed name
* for classes to be simple name instead of canonical.
*
* TODO: Added this in release 1.7. Delete this method in one of the next few release.
*
* @param token
* @param req
* @param rsp
* @return
*/
@Override
public Object getDynamic(String token, StaplerRequest req, StaplerResponse rsp) {
if (token.indexOf('.') == -1) {
return super.getDynamic(token, req, rsp);
}
if (this.classList != null) {
for (ClassResult classResult : this.classList) {
if (token.equals(classResult.getPkgName() + "." + classResult.getName())) {
return classResult;
}
}
}
return null;
}
}