package com.canoo.webtest.reporting;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.apache.tools.ant.BuildEvent;
import org.apache.tools.ant.Task;
import com.canoo.webtest.ant.IPropertyExpansionListener;
import com.canoo.webtest.ant.TestStepSequence;
import com.canoo.webtest.engine.Context;
import com.canoo.webtest.engine.ContextHelper;
import com.canoo.webtest.engine.MimeMap;
import com.canoo.webtest.engine.NOPBuildListener;
import com.canoo.webtest.engine.WebClientContext;
import com.canoo.webtest.steps.HtmlParserMessage;
import com.canoo.webtest.util.ConversionUtil;
import com.gargoylesoftware.htmlunit.FailingHttpStatusCodeException;
import com.gargoylesoftware.htmlunit.Page;
import com.gargoylesoftware.htmlunit.ScriptException;
import com.gargoylesoftware.htmlunit.WebResponse;
import com.gargoylesoftware.htmlunit.html.DomChangeEvent;
import com.gargoylesoftware.htmlunit.html.DomChangeListener;
import com.gargoylesoftware.htmlunit.html.HtmlPage;
import com.gargoylesoftware.htmlunit.util.Cookie;
/**
* Listens for task execution to extract {@link StepResult}s to generate the report.
*
* @author Marc Guillemot
*/
public class StepExecutionListener extends NOPBuildListener implements
IStepResultListener, IPropertyExpansionListener {
private static final Logger LOG = Logger.getLogger(StepExecutionListener.class);
private StepResult fCurrentResult;
private final HtmlParserMessage.MessageCollector fHtmlParserMessageCollector;
private final Context fContext;
private Page fPreviousCurrentResponse;
private WebClientContext.StoredResponses fPreviousResponses;
private boolean domChangedInLastPage = false;
private int resultIndex = 0;
private final Thread owningThread;
private final DomChangeListener domChangeListener = new DomChangeListener()
{
public void nodeAdded(final DomChangeEvent event)
{
domChangedInLastPage = true;
}
public void nodeDeleted(final DomChangeEvent event)
{
domChangedInLastPage = true;
}
};
private RootStepResult fRootResult;
private boolean fIgnoreCurrentTasks;
public StepExecutionListener(final Context context) {
owningThread = Thread.currentThread();
fContext = context;
if (context.getConfig().isShowHtmlParserOutput()) {
fHtmlParserMessageCollector = (HtmlParserMessage.MessageCollector) context
.getWebClient().getHTMLParserListener();
} else {
fHtmlParserMessageCollector = null;
}
}
/**
* Gets the html messages that have been catched since for the step beeing
* executed
*
* @return a (possibly empty) list of {@link HtmlParserMessage}
*/
protected List getLastHtmlParserMessages() {
if (fHtmlParserMessageCollector == null) {
return new ArrayList();
}
return fHtmlParserMessageCollector.popAll();
}
/**
* @return the fRootResult.
*/
public RootStepResult getRootResult() {
return fRootResult;
}
/**
* Indicates if report information should be captured for this task
*
* @param task the task
* @return <code>true</code> if report information should be captured
*/
protected boolean isInteresting(final Task task) {
if (fIgnoreCurrentTasks) {
LOG.debug("currently ignoring: " + task);
return false;
}
if (isToIgnore(task)) {
LOG.debug("toIgnore: " + task);
fIgnoreCurrentTasks = true;
return false;
}
return !"sequential".equals(task.getTaskName());
}
protected boolean isToIgnore(final Task task) {
// antlib, macrodef or property that are situated directly in the project are called
// when a <antcall /> is used within a webtest but should be ignored
return "antlib".equals(task.getTaskName());
}
/**
* @see com.canoo.webtest.ant.IPropertyExpansionListener#propertiesExpanded(java.lang.String,java.lang.String)
*/
public void propertiesExpanded(final String originalValue, final String expanded) {
if (!isEventForMe()) {
return;
}
fCurrentResult.propertiesExpanded(originalValue, expanded);
}
/**
* Called by {@link com.canoo.webtest.steps.Step} to notify computed values
* resulting of the execution of a step.
*
* @see com.canoo.webtest.reporting.IStepResultListener#stepResults(java.util.Map)
*/
public void stepResults(final Map results) {
if (!isEventForMe()) {
return;
}
fCurrentResult.addStepResults(results);
}
public void taskFinished(final BuildEvent event) {
if (!isEventForMe()) {
return;
}
final Task task = event.getTask();
LOG.trace("taskFinished: " + task.getTaskName(), event.getException());
if (isToIgnore(task)) { // current execution of taskdef implicitely generated by antlib used is finished
fIgnoreCurrentTasks = false;
return;
}
if (!isInteresting(task)) {
return;
}
if (fCurrentResult == null) {
throw new IllegalStateException("No current result");
}
final List liHtmlParserMessages = getLastHtmlParserMessages();
if (!fCurrentResult.isSuccessful()) {
final Throwable exception = event.getException();
// TODO: once reporting stabilized, generalize HtmlParserMessage to Message and
// add in exception message from inner steps into the reporting.
// if (exception != null) {
// final String message = exception.getMessage();
// if (message != null) liHtmlParserMessages.add(
// new HtmlParserMessage(HtmlParserMessage.Type.WARNING, UrlBoundary.tryCreateUrl("http://dummy.url"), message, 0, 0));
// }
fRootResult.setLastFailingTaskResult(fCurrentResult, exception);
}
fCurrentResult.taskFinished(task, event.getException(), liHtmlParserMessages);
saveCurrentResponseIfNeeded(event);
if (event.getException() != null && fPreviousResponses != null)
{
fContext.restoreResponses(fPreviousResponses);
fPreviousResponses = null;
}
fPreviousCurrentResponse = fContext.getCurrentResponse();
fCurrentResult = fCurrentResult.getParent();
}
private void saveCurrentResponseIfNeeded(final BuildEvent event)
{
if (!isSaveResponse()) {
return;
}
final String savePrefix = getSavePrefix();
final File file;
final WebResponse resp;
// new current response
if (isNewResponse(event))
{
if (isExceptionWithResponse(event))
{
final Throwable cause = event.getException().getCause();
if (cause instanceof FailingHttpStatusCodeException)
{
resp = ((FailingHttpStatusCodeException) cause).getResponse();
}
else
{
resp = ((ScriptException) cause).getPage().getWebResponse();
}
}
else
{
resp = fContext.getCurrentResponse().getWebResponse();
if (fContext.getCurrentResponse() instanceof HtmlPage) {
final HtmlPage page = (HtmlPage) fContext.getCurrentResponse();
page.addDomChangeListener(domChangeListener);
}
}
file = getResponseFile(resp, savePrefix, fCurrentResult.getTaskName());
ContextHelper.writeResponseFile(resp, file);
}
else if (domChangedInLastPage && fContext.getCurrentResponse() instanceof HtmlPage)
{
final HtmlPage page = (HtmlPage) fContext.getCurrentResponse();
resp = page.getWebResponse();
file = getResponseFile("html", savePrefix, fCurrentResult.getTaskName());
writeStringToFile(file, page.asXml());
}
else
{
// nothing to dump
return;
}
// write .info file related for the saved response to allow WebTestRecorder FF plugin to provide some functionalities
final File infoFile = new File(file.getParentFile(), file.getName() + ".info");
LOG.debug("Writing additional info to " + infoFile);
final StringBuilder sb = new StringBuilder();
sb.append("url=").append(resp.getWebRequest().getUrl()).append('\n');
final Set<Cookie> cookies = fContext.getWebClient().getCookieManager().getCookies();
sb.append("cookies=").append(cookies.size()).append('\n');
int i = 0;
for (final Cookie cookie : cookies) {
String prefix = "cookie." + (i++);
sb.append(prefix).append(".name=").append(cookie.getName()).append('\n');
sb.append(prefix).append(".domain=").append(cookie.getDomain()).append('\n');
sb.append(prefix).append(".value=").append(cookie.getValue()).append('\n');
sb.append(prefix).append(".path=").append(cookie.getPath()).append('\n');
}
writeStringToFile(infoFile, sb.toString());
fCurrentResult.getAttributes().put("resultFilename", file.getName());
domChangedInLastPage = false;
}
private void writeStringToFile(final File file, final String text) {
try
{
FileUtils.writeStringToFile(file, text, "UTF-8");
}
catch (final IOException e)
{
LOG.error("Failed to write to file " + file, e);
throw new RuntimeException("Failed to write reporting data", e);
}
}
private String getSavePrefix()
{
String prefix = fCurrentResult.getAttribute("save");
if (!StringUtils.isEmpty(prefix))
return prefix;
prefix = fCurrentResult.getAttribute("savePrefix");
return StringUtils.defaultIfEmpty(prefix, fContext.getConfig().getSavePrefix());
}
/**
* Gets the file in which the response should be written
*
* @param fileExtension the file extension
* @param fileNamePrefix the file prefix to use
* @return the file
*/
File getResponseFile(final String fileExtension, final String fileNamePrefix, final String fileNameSuffix) {
final int namespaceIndex = fileNameSuffix.indexOf(":");
final File resultDir = fContext.getConfig().getWebTestResultDir();
final String prefix = StringUtils.leftPad(String.valueOf(++resultIndex), 3, '0');
final String filename = prefix + "_" + fileNamePrefix
+ "_" + fileNameSuffix.substring(namespaceIndex == -1 ? 0 : namespaceIndex + 1)
+ "." + fileExtension;
return new File(resultDir, filename);
}
/**
* Gets the file in which the response should be written
*
* @param response the response to write
* @param fileNamePrefix the file prefix to use
* @return the file
*/
File getResponseFile(final WebResponse response, final String fileNamePrefix, final String fileNameSuffix) {
String contentType = response.getContentType();
contentType = MimeMap.adjustMimeTypeIfNeeded(contentType, response.getWebRequest().getUrl().toString());
final String extension = MimeMap.getExtension(contentType);
return getResponseFile(extension, fileNamePrefix, fileNameSuffix);
}
private boolean isExceptionWithResponse(final BuildEvent event)
{
if (event.getException() == null)
return false;
final Throwable cause = event.getException().getCause();
if (cause instanceof FailingHttpStatusCodeException)
return true;
else if (cause instanceof ScriptException)
{
final ScriptException se = (ScriptException) cause;
return se.getPage() != null; // should probably always be the case
// but a (now fixed) bug in HtmlUnit-1.14 causes an exception during init
// of the JavaScriptEngine and no page is available
}
return false;
}
/**
* Computes if actual current response as it has properties to be saved
* (for instance when it has changed)
*/
private boolean isNewResponse(final BuildEvent event)
{
LOG.debug("fPreviousCurrentResponse: " + fPreviousCurrentResponse);
LOG.debug("fContext.getCurrentResponse(): " + fContext.getCurrentResponse());
final boolean br = fPreviousCurrentResponse != fContext.getCurrentResponse();
LOG.debug("isWorthSaving: " + br + ", " + isExceptionWithResponse(event));
return (fContext.getCurrentResponse() != null
&& fPreviousCurrentResponse != fContext.getCurrentResponse())
|| isExceptionWithResponse(event);
}
private boolean isSaveResponse()
{
final String stepSave = fCurrentResult.getAttribute("save");
if (!StringUtils.isEmpty(stepSave)) {
return true;
}
final String stepSaveResponse = fCurrentResult.getAttribute("saveresponse");
if (!StringUtils.isEmpty(stepSaveResponse)) {
return ConversionUtil.convertToBoolean(stepSaveResponse, false);
}
return fContext.getConfig().isSaveResponse();
}
/**
* Called by Ant when a task is started.
* Captures the started task (in fact its wrapper) for report.
*
* @see org.apache.tools.ant.BuildListener#taskStarted(org.apache.tools.ant.BuildEvent)
*/
public void taskStarted(final BuildEvent event) {
if (!isEventForMe()) {
return;
}
final Task task = event.getTask();
if (!isInteresting(task)) {
return;
}
final StepResult result = new StepResult(task);
if (fCurrentResult != null) {
fCurrentResult.addChild(result);
fCurrentResult = result;
}
else {
fRootResult = new RootStepResult((TestStepSequence) task);
fCurrentResult = fRootResult;
}
fPreviousCurrentResponse = fContext.getCurrentResponse();
fPreviousResponses = fContext.getResponses();
}
/**
* Test if event is intended for this listener. When many threads run in parallel to execute <webtest>
* of the same project, the listeners will "see" the events for the other <webtest> and they should ignore them
*/
protected boolean isEventForMe() {
return owningThread == Thread.currentThread();
}
/**
* Called when the test execution finished. Here it does nothing, but it is intended for users extending this class.
*/
public void webtestFinished() {
}
}