/*
* The MIT License
*
* Copyright (c) 2013, Cisco Systems, Inc., a California corporation
*
* 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 org.jenkinsci.plugins.cucumber.jsontestsupport;
import hudson.model.Run;
import hudson.tasks.junit.CaseResult.Status;
import hudson.tasks.test.TestObject;
import hudson.tasks.test.TestResult;
import gherkin.formatter.model.Scenario;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import org.kohsuke.stapler.export.Exported;
import org.kohsuke.stapler.export.ExportedBean;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.servlet.ServletException;
/**
* Represents a Scenario belonging to a Feature from Cucumber.
*
* @author James Nord
*/
@ExportedBean
public class ScenarioResult extends TestResult {
private static final long serialVersionUID = 6813769160332278223L;
private static final Logger LOGGER = Logger.getLogger(ScenarioResult.class.getName());
private Scenario scenario;
private List<StepResult> steps = new ArrayList<StepResult>();
/** Possibly empty list of code executed before the Scenario. */
private List<BeforeAfterResult> beforeResults = new ArrayList<BeforeAfterResult>();
/** Possibly <code>null</code> Background executed before the Scenario. */
private BackgroundResult backgroundResult = null;
/** Possibly empty list of code executed before the Scenario. */
private List<BeforeAfterResult> afterResults = new ArrayList<BeforeAfterResult>();
/** Possibly empty list of embedded items for the Scenario. */
private List<EmbeddedItem> embeddedItems = new ArrayList<EmbeddedItem>();
private FeatureResult parent;
private transient Run<?, ?> owner;
private transient String safeName;
// true if this test failed
private transient boolean failed;
private transient boolean skipped;
private transient float duration;
/**
* This test has been failing since this build number (not id.)
*
* If {@link #isPassed() passing}, this field is left unused to 0.
*/
private int failedSince;
ScenarioResult(Scenario scenario, BackgroundResult backgroundResult) {
this.scenario = scenario;
this.backgroundResult = backgroundResult;
}
@Override
@Exported(visibility=9)
public String getName() {
return scenario.getName();
}
// XXX: getFullName was added in 1.594+
// when we bump core this should be tagged as an override.
/* @Override */
public String getFullName() {
return getParent().getName() + " \u01c2 " + getName();
}
/*
* Whilst a ScenarioResult contains a TestResult we do not count those individually. That would be akin to
* reporting each JUnit Assert as a test.
*/
@Override
public int getFailCount() {
return (failed ? 1 : 0);
}
@Override
public synchronized String getSafeName() {
if (safeName != null) {
return safeName;
}
String name = safe(scenario.getId());
String parentName = parent.getSafeName() + ';';
if (name.startsWith(parentName)) {
name = name.replace(parentName, "");
}
safeName = uniquifyName(parent.getChildren(), name);
return safeName;
}
@Override
public int getSkipCount() {
return (skipped ? 1 : 0);
}
@Override
@Exported(visibility=9)
public int getPassCount() {
// we are passed if we are not skipped and not failed.
return (skipped || failed) ? 0 : 1;
}
@Override
public Run<?, ?> getRun() {
return owner;
}
public void setOwner(Run<?, ?> owner) {
this.owner = owner;
for (BeforeAfterResult bar : beforeResults) {
bar.setOwner(owner);
}
for (BeforeAfterResult bar : afterResults) {
bar.setOwner(owner);
}
for (StepResult sr : steps) {
sr.setOwner(owner);
}
if (backgroundResult != null) {
backgroundResult.setOwner(owner);
}
}
@Override
public FeatureResult getParent() {
return parent;
}
protected void setParent(FeatureResult parent) {
this.parent = parent;
}
@Override
public TestResult findCorrespondingResult(String id) {
// we have no children so it is either us or null
if (id.equals(getId())) {
return this;
}
return null;
}
public String getDisplayName() {
return getName();
}
public BackgroundResult getBackgroundResult() {
return backgroundResult;
}
public List<BeforeAfterResult> getAfterResults() {
return afterResults;
}
void addAfterResult(BeforeAfterResult afterResult) {
afterResults.add(afterResult);
}
public List<BeforeAfterResult> getBeforeResults() {
return beforeResults;
}
void addBeforeResult(BeforeAfterResult beforeResult) {
beforeResults.add(beforeResult);
}
public List<EmbeddedItem> getEmbeddedItems() {
return embeddedItems;
}
void addEmbeddedItem(EmbeddedItem item) {
embeddedItems.add(item);
}
void addStepResult(StepResult stepResult) {
steps.add(stepResult);
}
public Collection<StepResult> getStepResults() {
return steps;
}
public Scenario getScenario() {
return scenario;
}
@Override
@Exported(visibility=9)
public float getDuration() {
return duration;
}
@Exported(name = "status", visibility = 9)
// stapler strips the trailing 's'
public Status getStatus() {
if (getSkipCount() > 0) {
// treat pending as skipped (undefined are errors).
return Status.SKIPPED;
}
ScenarioResult psr = (ScenarioResult) getPreviousResult();
if (psr == null) {
return isPassed() ? Status.PASSED : Status.FAILED;
}
if (psr.isPassed()) {
return isPassed() ? Status.PASSED : Status.REGRESSION;
}
else {
return isPassed() ? Status.FIXED : Status.FAILED;
}
}
/**
* If this test failed, then return the build number when this test started failing.
*/
@Override
@Exported(visibility = 9)
public int getFailedSince() {
// If we haven't calculated failedSince yet, and we should,
// do it now.
if (failedSince == 0 && getFailCount() == 1) {
ScenarioResult prev = (ScenarioResult) getPreviousResult();
if (prev != null && !prev.isPassed())
this.failedSince = prev.getFailedSince();
else if (getRun() != null) {
this.failedSince = getRun().getNumber();
}
else {
LOGGER.warning("Can not calculate failed since. we have a previous result but no owner.");
// failedSince will be 0, which isn't correct.
}
}
return failedSince;
}
/**
* 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 (getRun() != null) {
return getRun().getNumber() - getFailedSince() + 1;
}
else {
LOGGER.fine("Trying to get age of a ScenarioResult without an owner");
return 0;
}
}
@Override
public void tally() {
failed = false;
duration = 0.0f;
for (StepResult sr : steps) {
duration += sr.getDuration();
if (sr.getFailCount() != 0) {
failed = true;
}
if (sr.getSkipCount() != 0) {
skipped = true;
}
}
if (backgroundResult != null) {
backgroundResult.tally();
duration += backgroundResult.getDuration();
if (backgroundResult.getFailCount() != 0) {
failed = true;
}
if (backgroundResult.getSkipCount() != 0) {
skipped = true;
}
}
for (BeforeAfterResult bar : beforeResults) {
duration += bar.getDuration();
if (bar.getFailCount() != 0) {
failed = true;
}
if (bar.getSkipCount() != 0) {
skipped = true;
}
}
for (BeforeAfterResult bar : afterResults) {
duration += bar.getDuration();
if (bar.getFailCount() != 0) {
failed = true;
}
if (bar.getSkipCount() != 0) {
skipped = true;
}
}
// we can't be both skipped and failed - so failed takes precedence
if (failed) {
skipped = false;
}
}
/**
* If there was an error or a failure, this is the text from the message.
*/
public String getErrorDetails() {
// TODO - although we can only have one ErrorDetails
// and only one step can be a failure - we could have multiple
// undefined options - so we should list all the undefined options here..
if (!isPassed()) {
if(backgroundResult != null && !backgroundResult.isPassed()) {
for (StepResult step : backgroundResult.getStepResults()) {
if (!step.isPassed()) {
return step.getResult().getErrorMessage();
}
}
}
for (BeforeAfterResult before : getBeforeResults()) {
if (!before.isPassed()) {
return before.getResult().getErrorMessage();
}
}
for (StepResult step : getStepResults()) {
if (!step.isPassed()) {
return step.getResult().getErrorMessage();
}
}
for (BeforeAfterResult after : getAfterResults()) {
if (!after.isPassed()) {
return after.getResult().getErrorMessage();
}
}
}
return null;
}
public String getSource() {
return ScenarioToHTML.getHTML(this);
}
@Override
// Takes into account that this can be reached from a TagResult as well as a FeatureResult.
public String getRelativePathFrom(TestObject from) {
if (from == this) {
return ".";
}
String path = _getRelativePathFrom(from, this);
if (path == null) {
// try our parent as we could be coming indirectly from a tag not a Feature
path = _getRelativePathFrom(from.getParent(), this);
if (path != null) {
path = "../" + path;
}
}
if (path != null) {
return path;
}
return super.getRelativePathFrom(from);
}
private String _getRelativePathFrom(TestObject from, TestObject src) {
StringBuilder buf = new StringBuilder();
TestObject next = src;
TestObject cur = next;
// Walk up my ancestors from leaf to root, looking for "from"
// and accumulating a relative url as I go
while (next != null && from != next) {
cur = next;
buf.insert(0, '/');
buf.insert(0, cur.getSafeName());
next = cur.getParent();
}
if (from == next) {
return buf.toString();
}
return null;
}
@Override
@SuppressFBWarnings(value={"OBL_UNSATISFIED_OBLIGATION"}, justification="rsp.serveFile closes the stream")
public Object getDynamic(String token, StaplerRequest req, StaplerResponse rsp) {
if (token.equals(getId())) {
return this;
}
else if (token.startsWith("embed")) {
// lets go fishing...
String rest = req.getRestOfPath();
if (rest.startsWith("/")) {
rest = rest.substring(1);
// there won't be many embedded items so don't map them just search...
// is there enough here to display the thing??
for (EmbeddedItem item : getEmbeddedItems()) {
if (item.getFilename().equals(rest)) {
File file = new File(getRun().getRootDir(), "cucumber/embed/" + getParent().getSafeName() +
"/" +
getSafeName() + "/" + item.getFilename());
try {
FileInputStream fileInputStream = new FileInputStream(file);
rsp.serveFile(req, fileInputStream, file.lastModified(), Long.MAX_VALUE, file.length(),
"mime-type:" + item.getMimetype());
return null;
} catch (IOException ex) {
String msg = String.format("Failed to serve cucumber embedded file (%s) for in Feature " +
"(%s) for job (%s)",
item.getFilename(), this.getFullName(), this.getRun().getFullDisplayName());
LOGGER.log(Level.WARNING, msg, ex);
} catch (ServletException ex) {
String msg = String.format("Failed to serve cucumber embedded file (%s) for in Feature " +
"(%s) for job (%s)",
item.getFilename(), this.getFullName(), this.getRun().getFullDisplayName());
LOGGER.log(Level.WARNING, msg, ex);
}
return null;
}
}
}
}
return super.getDynamic(token, req, rsp);
}
}