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() { } }