package hudson.plugins.testng.results;
import java.io.IOException;
import java.util.*;
import hudson.model.Run;
import hudson.plugins.testng.TestNGTestResultBuildAction;
import hudson.plugins.testng.util.GraphHelper;
import hudson.tasks.test.TestResult;
import hudson.util.ChartUtil;
import hudson.util.DataSetBuilder;
import hudson.util.Graph;
import org.jfree.chart.JFreeChart;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import org.kohsuke.stapler.export.Exported;
/**
* Handles result pertaining to a single test method
*/
@SuppressWarnings("serial")
public class MethodResult extends BaseResult {
//pass, fail or skip status
private String status;
//test description if any from @Test annotation
private String description;
//is configuration method or not
private boolean isConfig;
//duration in seconds
private float duration;
//Exception thrown on running this test method (if any)
private MethodResultException exception;
//start time
private long startedAt;
//end time
private long endedAt;
//a test instance name if one is provided using ITest interface
private String testInstanceName;
//name of the <test> containing this method
private String parentTestName;
//name of the <suite> containing this method
private String parentSuiteName;
//groups this test method is part of
private List<String> groups;
//parameters passed into this test (if any)
private List<String> parameters;
// already stored with lines separated using <br/>
private String reporterOutput;
/**
* unique id for this test's run (helps associate the test method with
* related configuration methods)
*/
private String testRunId;
/**
* unique id for this test method
*/
private String testUuid;
public MethodResult(String name,
String status,
String description,
String duration,
long startedAt,
String isConfig,
String testRunId,
String parentTestName,
String parentSuiteName,
String testInstanceName) {
super(name);
this.status = status;
this.description = description;
// this uuid is used later to group the tests and config-methods together
this.testRunId = testRunId;
this.testInstanceName = testInstanceName;
this.parentTestName = parentTestName;
this.parentSuiteName = parentSuiteName;
this.startedAt = startedAt;
try {
long durationMs = Long.parseLong(duration);
//more accurate end time when test took less than a second to run
this.endedAt = startedAt + durationMs;
this.duration = (float) durationMs / 1000f;
} catch (NumberFormatException e) {
System.err.println("Unable to parse duration value: " + duration);
}
if (isConfig != null) {
/*
* If is-config attribute is present on test-method,
* it's always set to true
*/
this.isConfig = true;
}
}
public void setTestUuid(String testUuid) {
this.testUuid = testUuid;
}
/**
* @return name of the {@code <test>} tag that this method is part of
*/
public String getParentTestName() {
return parentTestName;
}
/**
* @return name of the suite this method is part of
*/
public String getParentSuiteName() {
return parentSuiteName;
}
public String getTestRunId() {
return testRunId;
}
@Exported
public Date getStartedAt() {
return new Date(startedAt);
}
public long getStartTime() {
return startedAt;
}
public long getEndTime() {
return endedAt;
}
public MethodResultException getException() {
return exception;
}
public void setException(MethodResultException exception) {
this.exception = exception;
}
/*
Overriding to add testUuid to name if a testUuid is present.
This is applicable only in cases of DataProvider tests
*/
@Override
public String getSafeName() {
String name = getName();
if (this.testUuid != null) {
name += "_" + this.testUuid;
}
return safe(name);
}
//special case for methods so overriding this method here
@Override
public TestResult findCorrespondingResult(String id) {
return getSafeName().equals(id) ? this : null;
}
/**
* 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
*/
@Override
@Exported
public float getDuration() {
return duration;
}
@Exported(visibility = 9)
public String getStatus() {
return status;
}
@Override
@Exported
public String getDescription() {
return description;
}
@Exported
public List<String> getGroups() {
return groups;
}
@Exported
public List<String> getParameters() {
return parameters;
}
/**
* Added only to expose possible exception via .../api/xxx
*
* @return String representation of the exception
*/
@Override
@Exported(name = "exception")
public String getErrorStackTrace() {
if (exception != null) {
return exception.toString();
}
return null;
}
/**
* If there was an error or a failure, this is the text from the message.
*/
@Override
public String getErrorDetails() {
if (exception != null) {
return exception.getMessage();
}
return null;
}
/**
* Added only to expose class name as part of method result via .../api/xxx
*
* @return String representation of the exception
*/
@Exported(name = "className")
public String getClassName() {
return getParent().getName();
}
public void setGroups(List<String> groups) {
this.groups = groups;
}
public void setParameters(List<String> parameters) {
this.parameters = parameters;
}
public boolean isConfig() {
return isConfig;
}
/**
* Creates test method execution history graph
*
* @param req request
* @param rsp response
* @throws IOException
*/
public void doGraph(final StaplerRequest req, StaplerResponse rsp) throws IOException {
Graph g = getGraph(req, rsp);
if (g != null) {
g.doPng(req, rsp);
}
}
/**
* Creates map to make the graph click-able
*
* @param req request
* @param rsp response
* @throws IOException
*/
public void doGraphMap(final StaplerRequest req, StaplerResponse rsp) throws IOException {
Graph g = getGraph(req, rsp);
if (g != null) {
g.doMap(req, rsp);
}
}
/**
* Returns graph instance if needed
*
* @param req request
* @param rsp response
* @return a graph
*/
private hudson.util.Graph getGraph(final StaplerRequest req, StaplerResponse rsp) {
Calendar t = getRun().getParent().getLastCompletedBuild().getTimestamp();
if (req.checkIfModified(t, rsp)) {
return null;
}
final DataSetBuilder<String, ChartUtil.NumberOnlyBuildLabel> dataSetBuilder =
new DataSetBuilder<String, ChartUtil.NumberOnlyBuildLabel>();
final Map<ChartUtil.NumberOnlyBuildLabel, String> statusMap =
new HashMap<ChartUtil.NumberOnlyBuildLabel, String>();
populateDataSetBuilder(dataSetBuilder, statusMap);
return new Graph(-1, 800, 150) {
protected JFreeChart createGraph() {
return GraphHelper.createMethodChart(req, dataSetBuilder.build(), statusMap,
// getUrl instead of getUpUrl as latter gets the complete url and we only need
// relative url path from a specific build
getUrl());
}
};
}
/**
* Populates the data set build with results from any successive and at max 9
* previous builds.
*
* @param dataSetBuilder the data set
* @param statusMap key as build and value as the execution status (result) of
* test method execution
*/
private void populateDataSetBuilder(
DataSetBuilder<String, ChartUtil.NumberOnlyBuildLabel> dataSetBuilder,
Map<ChartUtil.NumberOnlyBuildLabel, String> statusMap) {
int count = 0;
for (Run<?, ?> build = getRun(); build != null; build = build.getNextBuild()) {
addData(dataSetBuilder, statusMap, build);
}
for (Run<?, ?> build = getRun();
build != null && count++ < 10;
//getting running builds as well (will deal accordingly)
build = build.getPreviousBuild()) {
addData(dataSetBuilder, statusMap, build);
}
}
private void addData(DataSetBuilder<String, ChartUtil.NumberOnlyBuildLabel> dataSetBuilder,
Map<ChartUtil.NumberOnlyBuildLabel, String> statusMap,
Run<?, ?> build) {
ChartUtil.NumberOnlyBuildLabel label = new ChartUtil.NumberOnlyBuildLabel(build);
TestNGTestResultBuildAction action = build.getAction(TestNGTestResultBuildAction.class);
TestNGResult results;
MethodResult methodResult = null;
if (action != null && (results = action.getResult()) != null) {
methodResult = getMethodResult(results);
}
if (methodResult == null) {
dataSetBuilder.add(0, "resultRow", label);
//deal with builds still running
if (build.isBuilding()) {
statusMap.put(label, "BUILD IN PROGRESS");
} else {
statusMap.put(label, "UNKNOWN");
}
} else {
//status is PASS, FAIL or SKIP
dataSetBuilder.add(methodResult.getDuration(), "resultRow", label);
statusMap.put(label, methodResult.getStatus());
}
}
/**
* Gets the method result, if any, from the given set of test results. Searches
* for method result that matches the url of this method
*
* @param results test results
* @return method result
*/
private MethodResult getMethodResult(TestNGResult results) {
Map<String, PackageResult> packageMap = results.getPackageMap();
//get package name!
String methodPackageName = getParent().getParent().getName();
String methodClassName = getParent().getName();
if (packageMap.containsKey(methodPackageName) &&
packageMap.get(methodPackageName).getChildren() != null) {
List<ClassResult> classResults =
packageMap.get(methodPackageName).getChildren();
for (ClassResult classResult : classResults) {
if (classResult.getName().equals(methodClassName)) {
List<MethodResult> methodResults;
if (this.isConfig) {
methodResults = classResult.getConfigurationMethods();
} else {
methodResults = classResult.getTestMethods();
}
if (methodResults != null) {
for (MethodResult methodResult : methodResults) {
if (methodResult.getUrl().equals(this.getUrl())) {
return methodResult;
}
}
}
}
}
}
return null;
}
/**
* Used to give different color based on test status
*
* @return
*/
public Object getCssClass() {
if (this.status != null) {
if (this.status.equalsIgnoreCase("pass")) {
return "result-passed";
} else {
if (this.status.equalsIgnoreCase("skip")) {
return "result-skipped";
} else {
if (this.status.equalsIgnoreCase("fail")) {
return "result-failed";
}
}
}
}
return "result-passed";
}
/**
*
*/
public void setReporterOutput(String reporterOutput) {
this.reporterOutput = reporterOutput;
}
/**
* @return reporter output
*/
public String getReporterOutput() {
return reporterOutput;
}
@Override
public Collection<? extends TestResult> getChildren() {
return null;
}
@Override
public boolean hasChildren() {
return false;
}
}