001: // Copyright © 2002-2007 Canoo Engineering AG, Switzerland.
002: package com.canoo.webtest.ant;
003:
004: import java.util.HashMap;
005: import java.util.Iterator;
006: import java.util.Map;
007:
008: import org.apache.commons.lang.StringUtils;
009: import org.apache.log4j.Logger;
010: import org.apache.tools.ant.BuildException;
011: import org.apache.tools.ant.PropertyHelper;
012: import org.apache.tools.ant.Task;
013: import org.apache.tools.ant.TaskContainer;
014: import org.apache.tools.ant.UnsupportedElementException;
015:
016: import com.canoo.webtest.boundary.PackageBoundary;
017: import com.canoo.webtest.engine.Configuration;
018: import com.canoo.webtest.engine.Context;
019: import com.canoo.webtest.engine.WebClientContext;
020: import com.canoo.webtest.interfaces.IPropertyHandler;
021: import com.canoo.webtest.reporting.IResultReporter;
022: import com.canoo.webtest.reporting.PlainTextReporter;
023: import com.canoo.webtest.reporting.RootStepResult;
024: import com.canoo.webtest.reporting.StepExecutionListener;
025:
026: /**
027: * Ant task that specifies a Web Test Sequence.
028: *
029: * @author Unknown
030: * @author Marc Guillemot
031: * @webtest.step
032: * category="General"
033: * name="webtest"
034: * alias="testSpec"
035: * description="This <key>ANT</key> task provides the ability to specify
036: * and execute functional tests for web-based applications.
037: * The steps of the test specification to execute are defined as a sequence
038: * of nested test steps.
039: * Each <em><webtest></em> task is executed in its own web session,
040: * i.e. two subsequent <em><webtest></em> tasks are executed in different sessions.
041: * This task was previously named \"testSpec\". For compatibility reasons, both names will work."
042: */
043: public class WebtestTask extends Task implements TaskContainer,
044: IPropertyHandler {
045: private static final Logger LOG = Logger
046: .getLogger(WebtestTask.class);
047: private String fName;
048: private Configuration fConfig;
049: private TestStepSequence fSteps;
050: private boolean fImplicitSteps = true; // indicates that <steps> has been ommitted
051: public static final String REPORTER_CLASSNAME_PROPERTY = "webtest.resultreporterclass";
052: public static final String DEFAULT_REPORTER_CLASSNAME = "com.canoo.webtest.reporting.XmlReporter";
053: private final Map fDynamicProperties = new HashMap();
054: private static final ThreadLocal CONTEXT_HOLDER = new ThreadLocal();
055:
056: /**
057: * Gets the context to use for this thread.
058: * In a normal execution, this is the one that is created at the beginning of <webtest>
059: * and which should be used by all the steps from this webtest. These tests may
060: * be nested within macros, external targets, ... and may not "see" the <webtest> directly.<br/>
061: * A step doesn't need to call this method as it can access the context through {@link com.canoo.webtest.steps.Step#getContext()}.
062: * @return the currently used context, <code>null</code> if not inside the execution of a webtest.
063: */
064: public static Context getThreadContext() {
065: return (Context) CONTEXT_HOLDER.get();
066: }
067:
068: /**
069: * Sets the context for this thread.
070: * Normally this method should not be public and only the webtest should use it.
071: * Its visibility will be restricted once the unit tests have been adapted.
072: * @param context the context to set
073: */
074: public static void setThreadContext(final Context context) {
075: CONTEXT_HOLDER.set(context);
076: }
077:
078: public void setDynamicProperty(final String name, final String value) {
079: fDynamicProperties.put(name, value);
080: }
081:
082: public String getDynamicProperty(final String name) {
083: return (String) fDynamicProperties.get(name);
084: }
085:
086: public Map getDynamicProperties() {
087: return fDynamicProperties;
088: }
089:
090: /**
091: * Called by ant to add the nested "configuration ..." task.
092: *
093: * @param config the configuration task
094: * @webtest.nested.parameter
095: * required="no"
096: * description="The webtest configuration."
097: */
098: public void addConfig(final Configuration config) {
099: if (fSteps != null) {
100: final String msg = config.getTaskName()
101: + " invalid at this position! "
102: + "It has to be the first node of \""
103: + getTaskName() + "\"!";
104: throw new UnsupportedElementException(msg, config
105: .getTaskName());
106: }
107: fConfig = config;
108: fConfig.setPropertyHandler(this );
109: }
110:
111: /**
112: * In a first time doesn't support other tasks as config and testSpec, but
113: * this class has to implement {@link TaskContainer} to work with Groovy's AntBuilder
114: * (a bug in this AntBuilder?)
115: * @see org.apache.tools.ant.TaskContainer#addTask(org.apache.tools.ant.Task)
116: */
117: public void addTask(final Task task) {
118: LOG.debug("addTask: " + task.getTaskName() + " " + task);
119: if (task instanceof Configuration) {
120: addConfig((Configuration) task);
121: } else {
122: if (task instanceof TestStepSequence) {
123: addSteps((TestStepSequence) task);
124: } else if (!fImplicitSteps) {
125: throw new UnsupportedElementException(
126: "No step allowed after </steps>!", task
127: .getTaskName());
128: } else {
129: if (fSteps == null) {
130: final TestStepSequence implicitSteps = new TestStepSequence();
131: implicitSteps
132: .setDescription("Implicit <steps> task");
133: implicitSteps.setTaskName("steps");
134: implicitSteps.setProject(getProject());
135: implicitSteps.setOwningTarget(task
136: .getOwningTarget());
137: implicitSteps.setLocation(task.getLocation());
138: addSteps(implicitSteps);
139:
140: fImplicitSteps = true;
141: }
142: if (fImplicitSteps) {
143: // we have created <steps> by ourself, we need to populate its wrapper too
144: // as Ant would have done
145: getStepSequence()
146: .getRuntimeConfigurableWrapper()
147: .addChild(
148: task
149: .getRuntimeConfigurableWrapper());
150: }
151: getStepSequence().addTask(task);
152: }
153: }
154: }
155:
156: /**
157: * Called by ant to add the nested "webtest ..." task.
158: *
159: * @param steps the steps
160: * @webtest.nested.parameter
161: * required="yes"
162: * description="All the webtest steps."
163: */
164: public void addSteps(final TestStepSequence steps) {
165: // first create config if needed
166: if (getConfig() == null) {
167: addConfig(createDefaultConfiguration());
168: LOG
169: .info("No configuration defined, using default configuration.");
170: }
171:
172: if (fSteps != null) {
173: final String msg = getTaskName()
174: + " doesn't support multiple nested \""
175: + steps.getTaskName() + "\" elements.";
176: throw new UnsupportedElementException(msg, steps
177: .getTaskName());
178: }
179: fSteps = steps;
180: fImplicitSteps = false;
181: }
182:
183: /**
184: * Executes the task.
185: * If it doesn't contain a nested <config> a default one is created
186: * using {@link #createDefaultConfiguration()}.
187: */
188: public void execute() throws BuildException {
189: final String message = "webtest \"" + getName() + "\" ("
190: + getLocation() + ")";
191: LOG.info("Starting " + message);
192: final String webtestVersion = PackageBoundary.versionMessage();
193: LOG.info(webtestVersion);
194: getProject().setProperty("webtest.version", webtestVersion);
195:
196: assertParametersNotNull();
197: final Context context = new Context(this );
198: CONTEXT_HOLDER.set(context);
199:
200: LOG.debug("Executing configuration task");
201: getConfig().setContext(context);
202: getConfig().perform();
203:
204: // register custom property helper in place of the original one
205: final PropertyHelper originalPropertyHelper = PropertyHelper
206: .getPropertyHelper(getProject());
207:
208: WebtestPropertyHelper
209: .configureWebtestPropertyHelper(getProject());
210:
211: // register the listener that will capture the results
212: fResultBuilderListener = new StepExecutionListener(context);
213: getProject().addBuildListener(fResultBuilderListener);
214:
215: try {
216: fSteps.perform();
217: } catch (final BuildException e) {
218: // nothing, exception is available in result build listener too
219: } finally {
220: getProject().removeBuildListener(fResultBuilderListener);
221: WebtestPropertyHelper.definePropertyHelper(getProject(),
222: originalPropertyHelper);
223:
224: // clean the WebClient(s) to stop running js scripts (like setTimeout)
225: for (final Iterator iter = context.getWebClientContexts()
226: .values().iterator(); iter.hasNext();) {
227: final WebClientContext webClientContext = (WebClientContext) iter
228: .next();
229: webClientContext.destroy();
230: }
231: }
232:
233: LOG.info("Finished executing " + message);
234:
235: writeTestReportIfNeeded(fResultBuilderListener.getRootResult());
236: stopBuildIfNeeded(fResultBuilderListener.getRootResult(),
237: fConfig);
238: }
239:
240: private StepExecutionListener fResultBuilderListener;
241:
242: /**
243: * TODO: check if it should really be accessible
244: * @return the listener that will build the results
245: */
246: protected StepExecutionListener getResultBuilderListener() {
247: return fResultBuilderListener;
248: }
249:
250: /**
251: * Creates the default configuration to use if no <configuration> was
252: * present in the ant file.
253: * @return the configuration
254: */
255: protected Configuration createDefaultConfiguration() {
256: final Configuration configuration = new Configuration(this );
257: LOG.debug("Default configuration created: host="
258: + configuration.getHost() + ", port="
259: + configuration.getPort() + ", protocol="
260: + configuration.getProtocol());
261: return configuration;
262: }
263:
264: protected void stopBuildIfNeeded(
265: final RootStepResult webTestResult,
266: final Configuration config) {
267: LOG.debug("Looking if it is needed to stop the build");
268: if (webTestResult.isError() && config.isHaltOnError()
269: || webTestResult.isFailure()
270: && config.isHaltOnFailure()) {
271: LOG
272: .debug("Exception: "
273: + webTestResult.getException().getClass()
274: .getName());
275: LOG.debug("Throwing BuildException");
276: if (webTestResult.getException() instanceof BuildException) {
277: throw (BuildException) webTestResult.getException();
278: } else {
279: final String str = PlainTextReporter
280: .getBuildFailMessage(webTestResult);
281: LOG.debug("str: " + str);
282: throw new BuildException(webTestResult.getException());
283: }
284: }
285: if (webTestResult.isError()
286: && !StringUtils.isEmpty(config.getErrorProperty())) {
287: LOG.debug("Set error property \""
288: + config.getErrorProperty() + "\" to true");
289: getProject().setProperty(config.getErrorProperty(), "true");
290: }
291: if (webTestResult.isFailure()
292: && !StringUtils.isEmpty(config.getFailureProperty())) {
293: LOG.debug("Set failure property \""
294: + config.getFailureProperty() + "\" to true");
295: getProject().setProperty(config.getFailureProperty(),
296: "true");
297: }
298: }
299:
300: // *********************************************************************
301: // Implementation of the IPropertyHandler interface
302: // *********************************************************************
303: public String getProperty(final String propertyName) {
304: return getProject().getProperty(propertyName);
305: }
306:
307: private void assertParametersNotNull() throws BuildException {
308: assertAttributeNotNull(fName, "name");
309: assertNestedElementNotNull(fSteps, "steps");
310: }
311:
312: private void assertAttributeNotNull(final Object parameter,
313: final String parameterName) {
314: final String[] msg = { "attribute ", "\n", parameterName, "\n" };
315: assertNotNull(parameter, msg);
316: }
317:
318: private void assertNestedElementNotNull(final Object parameter,
319: final String parameterName) {
320: final String[] msg = { "nested element ", "<", parameterName,
321: ">" };
322: assertNotNull(parameter, msg);
323: }
324:
325: protected void assertNotNull(final Object parameter,
326: final String[] msg) {
327: if (parameter == null) {
328: throw new BuildException("Required " + msg[0] + msg[1]
329: + msg[2] + msg[3] + " is not set!");
330: }
331: }
332:
333: /**
334: * @param name
335: * @webtest.parameter
336: * required="yes"
337: * description="Defines a name for this test specification."
338: */
339: public void setName(final String name) {
340: fName = name;
341: }
342:
343: /**
344: * gets the name of this webtest
345: * @return the name (as specified in <webtest name="...">)
346: */
347: public String getName() {
348: return fName;
349: }
350:
351: protected void writeTestReportIfNeeded(final RootStepResult result) {
352: if (!fConfig.isSummary()) {
353: LOG.info("No report to write according to config");
354: return;
355: }
356: String reporterClass = getProject().getProperty(
357: REPORTER_CLASSNAME_PROPERTY);
358: if (reporterClass == null) {
359: reporterClass = DEFAULT_REPORTER_CLASSNAME;
360: }
361: LOG.debug("Writing test report using Report class: "
362: + reporterClass);
363: callSelectedReporter(reporterClass, result);
364: LOG.debug("Report written");
365: }
366:
367: protected void callSelectedReporter(final String reporterClass,
368: final RootStepResult result) {
369: try {
370: final IResultReporter reporter = (IResultReporter) Class
371: .forName(reporterClass).newInstance();
372: report(reporter, result);
373: } catch (final Exception e) {
374: LOG.error("Exception caught while writing test report", e);
375: }
376: }
377:
378: protected void report(final IResultReporter reporter,
379: final RootStepResult result) {
380: try {
381: reporter.generateReport(result);
382: LOG.info("Test report successfully created.");
383: } catch (final Exception e) {
384: LOG.error("Exception caught while writing test report", e);
385: }
386: }
387:
388: public Configuration getConfig() {
389: return fConfig;
390: }
391:
392: protected void setConfig(final Configuration config) {
393: fConfig = config;
394: }
395:
396: public TestStepSequence getStepSequence() {
397: return fSteps;
398: }
399: }
|