001: package com.canoo.webtest.reporting;
002:
003: import java.io.File;
004: import java.io.IOException;
005: import java.util.ArrayList;
006: import java.util.List;
007: import java.util.Map;
008:
009: import org.apache.commons.io.FileUtils;
010: import org.apache.commons.lang.StringUtils;
011: import org.apache.log4j.Logger;
012: import org.apache.tools.ant.BuildEvent;
013: import org.apache.tools.ant.Task;
014:
015: import com.canoo.webtest.ant.IPropertyExpansionListener;
016: import com.canoo.webtest.ant.TestStepSequence;
017: import com.canoo.webtest.engine.Context;
018: import com.canoo.webtest.engine.ContextHelper;
019: import com.canoo.webtest.engine.MimeMap;
020: import com.canoo.webtest.engine.NOPBuildListener;
021: import com.canoo.webtest.engine.WebClientContext;
022: import com.canoo.webtest.steps.HtmlParserMessage;
023: import com.canoo.webtest.util.ConversionUtil;
024: import com.gargoylesoftware.htmlunit.FailingHttpStatusCodeException;
025: import com.gargoylesoftware.htmlunit.Page;
026: import com.gargoylesoftware.htmlunit.ScriptException;
027: import com.gargoylesoftware.htmlunit.WebResponse;
028: import com.gargoylesoftware.htmlunit.html.DomChangeEvent;
029: import com.gargoylesoftware.htmlunit.html.DomChangeListener;
030: import com.gargoylesoftware.htmlunit.html.HtmlPage;
031:
032: /**
033: * Listens for task execution to extract {@link StepResult}s to generate the report.
034: *
035: * @author Marc Guillemot
036: */
037: public class StepExecutionListener extends NOPBuildListener implements
038: IStepResultListener, IPropertyExpansionListener {
039: private static final Logger LOG = Logger
040: .getLogger(StepExecutionListener.class);
041: private StepResult fCurrentResult;
042: private final HtmlParserMessage.MessageCollector fHtmlParserMessageCollector;
043: private final Context fContext;
044: private Page fPreviousCurrentResponse;
045: private WebClientContext.StoredResponses fPreviousResponses;
046: private boolean domChangedInLastPage = false;
047: private int resultIndex = 0;
048:
049: private final DomChangeListener domChangeListener = new DomChangeListener() {
050: public void nodeAdded(final DomChangeEvent event) {
051: domChangedInLastPage = true;
052: }
053:
054: public void nodeDeleted(final DomChangeEvent event) {
055: domChangedInLastPage = true;
056: }
057: };
058:
059: private RootStepResult fRootResult;
060: private boolean fIgnoreCurrentTasks;
061:
062: public StepExecutionListener(final Context context) {
063: fContext = context;
064: if (context.getConfig().isShowHtmlParserOutput()) {
065: fHtmlParserMessageCollector = (HtmlParserMessage.MessageCollector) context
066: .getWebClient().getHTMLParserListener();
067: } else {
068: fHtmlParserMessageCollector = null;
069: }
070: }
071:
072: /**
073: * Gets the html messages that have been catched since for the step beeing
074: * executed
075: *
076: * @return a (possibly empty) list of {@link HtmlParserMessage}
077: */
078: protected List getLastHtmlParserMessages() {
079: if (fHtmlParserMessageCollector == null) {
080: return new ArrayList();
081: }
082: return fHtmlParserMessageCollector.popAll();
083: }
084:
085: /**
086: * @return the fRootResult.
087: */
088: public RootStepResult getRootResult() {
089: return fRootResult;
090: }
091:
092: /**
093: * Indicates if report information should be captured for this task
094: *
095: * @param task the task
096: * @return <code>true</code> if report information should be captured
097: */
098: protected boolean isInteresting(final Task task) {
099: if (fIgnoreCurrentTasks) {
100: LOG.debug("currently ignoring: " + task);
101: return false;
102: }
103: if (isToIgnore(task)) {
104: LOG.debug("toIgnore: " + task);
105: fIgnoreCurrentTasks = true;
106: return false;
107: }
108: return !"sequential".equals(task.getTaskName());
109: }
110:
111: protected boolean isToIgnore(final Task task) {
112: // antlib, macrodef or property that are situated directly in the project are called
113: // when a <antcall /> is used within a webtest but should be ignored
114: return "antlib".equals(task.getTaskName());
115: }
116:
117: /**
118: * @see com.canoo.webtest.ant.IPropertyExpansionListener#propertiesExpanded(java.lang.String,java.lang.String)
119: */
120: public void propertiesExpanded(final String originalValue,
121: final String expanded) {
122: fCurrentResult.propertiesExpanded(originalValue, expanded);
123: }
124:
125: /**
126: * Called by {@link com.canoo.webtest.steps.Step} to notify computed values
127: * resulting of the execution of a step.
128: *
129: * @see com.canoo.webtest.reporting.IStepResultListener#stepResults(java.util.Map)
130: */
131: public void stepResults(final Map results) {
132: fCurrentResult.addStepResults(results);
133: }
134:
135: public void taskFinished(final BuildEvent event) {
136: final Task task = event.getTask();
137: LOG.trace("taskFinished: " + task.getTaskName(), event
138: .getException());
139:
140: if (isToIgnore(task)) { // current execution of taskdef implicitely generated by antlib used is finished
141: fIgnoreCurrentTasks = false;
142: return;
143: }
144: if (!isInteresting(task)) {
145: return;
146: }
147:
148: if (fCurrentResult == null) {
149: throw new IllegalStateException("No current result");
150: }
151:
152: final List liHtmlParserMessages = getLastHtmlParserMessages();
153:
154: if (!fCurrentResult.isSuccessful()) {
155: final Throwable exception = event.getException();
156: // TODO: once reporting stabilized, generalize HtmlParserMessage to Message and
157: // add in exception message from inner steps into the reporting.
158: // if (exception != null) {
159: // final String message = exception.getMessage();
160: // if (message != null) liHtmlParserMessages.add(
161: // new HtmlParserMessage(HtmlParserMessage.Type.WARNING, UrlBoundary.tryCreateUrl("http://dummy.url"), message, 0, 0));
162: // }
163: fRootResult.setLastFailingTaskResult(fCurrentResult,
164: exception);
165: }
166: fCurrentResult.taskFinished(task, event.getException(),
167: liHtmlParserMessages);
168:
169: saveCurrentResponseIfNeeded(event);
170:
171: if (event.getException() != null && fPreviousResponses != null) {
172: fContext.restoreResponses(fPreviousResponses);
173: fPreviousResponses = null;
174: }
175: fPreviousCurrentResponse = fContext.getCurrentResponse();
176:
177: fCurrentResult = fCurrentResult.getParent();
178: }
179:
180: private void saveCurrentResponseIfNeeded(final BuildEvent event) {
181: if (!isSaveResponse()) {
182: return;
183: }
184:
185: final String savePrefix = getSavePrefix();
186: final File file;
187:
188: // new current response
189: if (isNewResponse(event)) {
190: final WebResponse resp;
191: if (isExceptionWithResponse(event)) {
192: final Throwable cause = event.getException().getCause();
193: if (cause instanceof FailingHttpStatusCodeException) {
194: resp = ((FailingHttpStatusCodeException) cause)
195: .getResponse();
196: } else {
197: resp = ((ScriptException) cause).getPage()
198: .getWebResponse();
199: }
200: } else {
201: resp = fContext.getCurrentResponse().getWebResponse();
202: if (fContext.getCurrentResponse() instanceof HtmlPage) {
203: final HtmlPage page = (HtmlPage) fContext
204: .getCurrentResponse();
205: page.addDomChangeListener(domChangeListener);
206: }
207: }
208:
209: file = getResponseFile(resp, savePrefix, fCurrentResult
210: .getTaskName());
211: ContextHelper.writeResponseFile(resp, file);
212: } else if (domChangedInLastPage
213: && fContext.getCurrentResponse() instanceof HtmlPage) {
214: final HtmlPage page = (HtmlPage) fContext
215: .getCurrentResponse();
216: file = getResponseFile("html", savePrefix, fCurrentResult
217: .getTaskName());
218: try {
219: FileUtils.writeStringToFile(file, page.asXml());
220: } catch (final IOException e) {
221: LOG.error("Failed to dump current page state to file",
222: e);
223: }
224: } else {
225: // nothing to dump
226: return;
227: }
228:
229: fCurrentResult.getAttributes().put("resultFilename",
230: file.getName());
231: domChangedInLastPage = false;
232: }
233:
234: private String getSavePrefix() {
235: String prefix = fCurrentResult.getAttribute("save");
236: if (!StringUtils.isEmpty(prefix))
237: return prefix;
238:
239: prefix = fCurrentResult.getAttribute("savePrefix");
240: return StringUtils.defaultIfEmpty(prefix, fContext.getConfig()
241: .getSavePrefix());
242: }
243:
244: /**
245: * Gets the file in which the response should be written
246: *
247: * @param fileExtension the file extension
248: * @param fileNamePrefix the file prefix to use
249: * @return the file
250: */
251: File getResponseFile(final String fileExtension,
252: final String fileNamePrefix, final String fileNameSuffix) {
253: final int namespaceIndex = fileNameSuffix.indexOf(":");
254: final File resultDir = fContext.getConfig()
255: .getWebTestResultDir();
256:
257: final String prefix = StringUtils.leftPad(String
258: .valueOf(++resultIndex), 3, '0');
259:
260: final String filename = prefix
261: + "_"
262: + fileNamePrefix
263: + "_"
264: + fileNameSuffix.substring(namespaceIndex == -1 ? 0
265: : namespaceIndex + 1) + "." + fileExtension;
266: return new File(resultDir, filename);
267: }
268:
269: /**
270: * Gets the file in which the response should be written
271: *
272: * @param response the response to write
273: * @param fileNamePrefix the file prefix to use
274: * @return the file
275: */
276: File getResponseFile(final WebResponse response,
277: final String fileNamePrefix, final String fileNameSuffix) {
278: String contentType = response.getContentType();
279: contentType = MimeMap.adjustMimeTypeIfNeeded(contentType,
280: response.getUrl().toString());
281: final String extension = MimeMap.getExtension(contentType);
282:
283: return getResponseFile(extension, fileNamePrefix,
284: fileNameSuffix);
285: }
286:
287: private boolean isExceptionWithResponse(final BuildEvent event) {
288: if (event.getException() == null)
289: return false;
290:
291: final Throwable cause = event.getException().getCause();
292: if (cause instanceof FailingHttpStatusCodeException)
293: return true;
294: else if (cause instanceof ScriptException) {
295: final ScriptException se = (ScriptException) cause;
296: return se.getPage() != null; // should probably always be the case
297: // but a (now fixed) bug in HtmlUnit-1.14 causes an exception during init
298: // of the JavaScriptEngine and no page is available
299: }
300: return false;
301: }
302:
303: /**
304: * Computes if actual current response as it has properties to be saved
305: * (for instance when it has changed)
306: */
307: private boolean isNewResponse(final BuildEvent event) {
308: LOG.debug("fPreviousCurrentResponse: "
309: + fPreviousCurrentResponse);
310: LOG.debug("fContext.getCurrentResponse(): "
311: + fContext.getCurrentResponse());
312: final boolean br = fPreviousCurrentResponse != fContext
313: .getCurrentResponse();
314: LOG.debug("isWorthSaving: " + br + ", "
315: + isExceptionWithResponse(event));
316: return (fContext.getCurrentResponse() != null && fPreviousCurrentResponse != fContext
317: .getCurrentResponse())
318: || isExceptionWithResponse(event);
319: }
320:
321: private boolean isSaveResponse() {
322: final String stepSave = fCurrentResult.getAttribute("save");
323: if (!StringUtils.isEmpty(stepSave)) {
324: return true;
325: }
326: final String stepSaveResponse = fCurrentResult
327: .getAttribute("saveresponse");
328: if (!StringUtils.isEmpty(stepSaveResponse)) {
329: return ConversionUtil.convertToBoolean(stepSaveResponse,
330: false);
331: }
332: return fContext.getConfig().isSaveResponse();
333: }
334:
335: /**
336: * Called by Ant when a task is started.
337: * Captures the started task (in fact its wrapper) for report.
338: *
339: * @see org.apache.tools.ant.BuildListener#taskStarted(org.apache.tools.ant.BuildEvent)
340: */
341: public void taskStarted(final BuildEvent event) {
342: final Task task = event.getTask();
343: if (!isInteresting(task)) {
344: return;
345: }
346:
347: final StepResult result = new StepResult(task);
348: if (fCurrentResult != null) {
349: fCurrentResult.addChild(result);
350: fCurrentResult = result;
351: } else {
352: fRootResult = new RootStepResult((TestStepSequence) task);
353: fCurrentResult = fRootResult;
354: }
355: fPreviousCurrentResponse = fContext.getCurrentResponse();
356: fPreviousResponses = fContext.getResponses();
357: }
358: }
|