001: /*
002: * ========================================================================
003: *
004: * Copyright 2001-2005 The Apache Software Foundation.
005: *
006: * Licensed under the Apache License, Version 2.0 (the "License");
007: * you may not use this file except in compliance with the License.
008: * You may obtain a copy of the License at
009: *
010: * http://www.apache.org/licenses/LICENSE-2.0
011: *
012: * Unless required by applicable law or agreed to in writing, software
013: * distributed under the License is distributed on an "AS IS" BASIS,
014: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015: * See the License for the specific language governing permissions and
016: * limitations under the License.
017: *
018: * ========================================================================
019: */
020: package org.apache.cactus.internal.server.runner;
021:
022: import java.text.NumberFormat;
023: import java.util.Locale;
024:
025: import junit.framework.AssertionFailedError;
026: import junit.framework.Test;
027: import junit.framework.TestFailure;
028: import junit.framework.TestListener;
029: import junit.framework.TestResult;
030:
031: import org.apache.cactus.internal.util.JUnitVersionHelper;
032: import org.apache.cactus.internal.util.StringUtil;
033:
034: /**
035: * Format the test results in XML.
036: *
037: * @version $Id: XMLFormatter.java 239169 2005-05-05 09:21:54Z vmassol $
038: */
039: public class XMLFormatter implements XMLConstants, TestListener {
040: /**
041: * Default stack filter patterns.
042: */
043: private static final String[] DEFAULT_STACK_FILTER_PATTERNS = new String[] {
044: "org.apache.cactus.AbstractTestCase",
045: "org.apache.cactus.AbstractWebTestCase",
046: "org.apache.cactus.FilterTestCase",
047: "org.apache.cactus.JspTestCase",
048: "org.apache.cactus.ServletTestCase",
049: "junit.framework.TestCase", "junit.framework.TestResult",
050: "junit.framework.TestSuite", "junit.framework.Assert.", // don't filter AssertionFailure
051: "java.lang.reflect.Method.invoke(" };
052:
053: /**
054: * (optional) Name of the XSL stylesheet to put in the returned XML string
055: * so that the browser will try to apply it (IE at least, I don't know
056: * about the others).
057: */
058: private String xslFileName;
059:
060: /**
061: * The name of the test suite class.
062: */
063: private String suiteClassName;
064:
065: /**
066: * Duration it took to execute all the tests.
067: */
068: private long totalDuration;
069:
070: /**
071: * Encoding to use for the returned XML. Defaults to "UTF-8".
072: */
073: private String encoding = "UTF-8";
074:
075: /**
076: * Time current test was started
077: */
078: private long currentTestStartTime;
079:
080: /**
081: * The number format used to convert durations into strings. Don't use the
082: * default locale for that, because the resulting string needs to use
083: * dotted decimal notation for an XSLT transformation to work correctly.
084: */
085: private NumberFormat durationFormat = NumberFormat
086: .getInstance(Locale.US);
087:
088: /**
089: * XML string containing executed test case results
090: */
091: private StringBuffer currentTestCaseResults = new StringBuffer();
092:
093: /**
094: * Current test failure (XML string) : failure or error.
095: */
096: private String currentTestFailure;
097:
098: /**
099: * Sets the XSL stylesheet file name to put in the returned XML string
100: * so that the browser will try to apply it (IE at least, I don't know
101: * about the others).
102: *
103: * @param theXslFileName the file name (relative to the webapp root)
104: */
105: public void setXslFileName(String theXslFileName) {
106: this .xslFileName = theXslFileName;
107: }
108:
109: /**
110: * @param theEncoding the encoding to use for the returned XML.
111: */
112: public void setEncoding(String theEncoding) {
113: this .encoding = theEncoding;
114: }
115:
116: /**
117: * @return the encoding to use for the returned XML
118: */
119: public String getEncoding() {
120: return this .encoding;
121: }
122:
123: /**
124: * @return the suite class name
125: */
126: public String getSuiteClassName() {
127: return this .suiteClassName;
128: }
129:
130: /**
131: * Sets the suite class name that was executed.
132: *
133: * @param theSuiteClassName the suite class name
134: */
135: public void setSuiteClassName(String theSuiteClassName) {
136: this .suiteClassName = theSuiteClassName;
137: }
138:
139: /**
140: * @return the total duration as a string
141: */
142: public String getTotalDurationAsString() {
143: return getDurationAsString(this .totalDuration);
144: }
145:
146: /**
147: * Comvert a duration expressed as a long into a string.
148: *
149: * @param theDuration the duration to convert to string
150: * @return the total duration as a string
151: */
152: private String getDurationAsString(long theDuration) {
153: return durationFormat.format((double) theDuration / 1000);
154: }
155:
156: /**
157: * Sets the duration it took to execute all the tests.
158: *
159: * @param theDuration the time it took
160: */
161: public void setTotalDuration(long theDuration) {
162: this .totalDuration = theDuration;
163: }
164:
165: /**
166: * Formats the test result as an XML string.
167: *
168: * @param theResult the test result object
169: * @return the XML string representation of the test results
170: */
171: public String toXML(TestResult theResult) {
172: StringBuffer xml = new StringBuffer();
173:
174: xml.append("<?xml version=\"1.0\" encoding=\"" + getEncoding()
175: + "\"?>");
176:
177: if (this .xslFileName != null) {
178: xml.append("<?xml-stylesheet type=\"text/xsl\" "
179: + "href=\"" + this .xslFileName + "\"?>");
180: }
181:
182: xml.append("<" + TESTSUITES + ">");
183:
184: xml.append("<" + TESTSUITE + " " + ATTR_NAME + "=\""
185: + getSuiteClassName() + "\" " + ATTR_TESTS + "=\""
186: + theResult.runCount() + "\" " + ATTR_FAILURES + "=\""
187: + theResult.failureCount() + "\" " + ATTR_ERRORS
188: + "=\"" + theResult.errorCount() + "\" " + ATTR_TIME
189: + "=\"" + getTotalDurationAsString() + "\">");
190:
191: xml.append(this .currentTestCaseResults.toString());
192:
193: xml.append("</" + TESTSUITE + ">");
194: xml.append("</" + TESTSUITES + ">");
195:
196: return xml.toString();
197: }
198:
199: /**
200: * Event called by the base test runner when the test starts.
201: *
202: * @param theTest the test object being executed
203: */
204: public void startTest(Test theTest) {
205: this .currentTestStartTime = System.currentTimeMillis();
206: this .currentTestFailure = null;
207: }
208:
209: /**
210: * Event called by the base test runner when the test fails with an error.
211: *
212: * @param theTest the test object that failed
213: * @param theThrowable the exception that was thrown
214: */
215: public void addError(Test theTest, Throwable theThrowable) {
216: TestFailure failure = new TestFailure(theTest, theThrowable);
217: StringBuffer xml = new StringBuffer();
218:
219: xml.append("<" + ERROR + " " + ATTR_MESSAGE + "=\""
220: + xmlEncode(failure.thrownException().getMessage())
221: + "\" " + ATTR_TYPE + "=\""
222: + failure.thrownException().getClass().getName()
223: + "\">");
224: xml.append(xmlEncode(StringUtil.exceptionToString(failure
225: .thrownException(), DEFAULT_STACK_FILTER_PATTERNS)));
226: xml.append("</" + ERROR + ">");
227:
228: this .currentTestFailure = xml.toString();
229: }
230:
231: /**
232: * Event called by the base test runner when the test fails with a failure.
233: *
234: * @param theTest the test object that failed
235: * @param theError the exception that was thrown
236: */
237: public void addFailure(Test theTest, AssertionFailedError theError) {
238: TestFailure failure = new TestFailure(theTest, theError);
239: StringBuffer xml = new StringBuffer();
240:
241: xml.append("<" + FAILURE + " " + ATTR_MESSAGE + "=\""
242: + xmlEncode(failure.thrownException().getMessage())
243: + "\" " + ATTR_TYPE + "=\""
244: + failure.thrownException().getClass().getName()
245: + "\">");
246: xml.append(xmlEncode(StringUtil.exceptionToString(failure
247: .thrownException(), DEFAULT_STACK_FILTER_PATTERNS)));
248: xml.append("</" + FAILURE + ">");
249:
250: this .currentTestFailure = xml.toString();
251: }
252:
253: /**
254: * Event called by the base test runner when the test ends.
255: *
256: * @param theTest the test object being executed
257: */
258: public void endTest(Test theTest) {
259: StringBuffer xml = new StringBuffer();
260: String duration = getDurationAsString(System
261: .currentTimeMillis()
262: - this .currentTestStartTime);
263:
264: xml.append("<" + TESTCASE + " " + ATTR_NAME + "=\""
265: + JUnitVersionHelper.getTestCaseName(theTest) + "\" "
266: + ATTR_TIME + "=\"" + duration + "\">");
267:
268: if (this .currentTestFailure != null) {
269: xml.append(this .currentTestFailure);
270: }
271:
272: xml.append("</" + TESTCASE + ">");
273:
274: this .currentTestCaseResults.append(xml.toString());
275: }
276:
277: /**
278: * Escapes reserved XML characters.
279: *
280: * @param theString the string to escape
281: * @return the escaped string
282: */
283: private String xmlEncode(String theString) {
284: String newString;
285:
286: // It is important to replace the "&" first as the other replacements
287: // also introduces "&" chars ...
288: newString = StringUtil.replace(theString, '&', "&");
289:
290: newString = StringUtil.replace(newString, '<', "<");
291: newString = StringUtil.replace(newString, '>', ">");
292: newString = StringUtil.replace(newString, '\"', """);
293:
294: return newString;
295: }
296: }
|