001: package org.testng.reporters;
002:
003: import java.io.BufferedWriter;
004: import java.io.File;
005: import java.io.FileWriter;
006: import java.io.IOException;
007:
008: import java.util.ArrayList;
009: import java.util.Collections;
010: import java.util.HashMap;
011: import java.util.List;
012: import java.util.Map;
013: import java.util.Properties;
014: import java.util.regex.Matcher;
015: import java.util.regex.Pattern;
016:
017: import org.testng.ITestContext;
018: import org.testng.ITestResult;
019: import org.testng.internal.IResultListener;
020: import org.testng.internal.Utils;
021:
022: /**
023: * A JUnit XML report generator (replacing the original JUnitXMLReporter that was
024: * based on XML APIs).
025: *
026: * @author <a href='mailto:the[dot]mindstorm[at]gmail[dot]com'>Alex Popescu</a>
027: */
028: public class JUnitXMLReporter implements IResultListener {
029: private static final Pattern ENTITY = Pattern
030: .compile("&[a-zA-Z]+;.*");
031: private static final Pattern LESS = Pattern.compile("<");
032: private static final Pattern GREATER = Pattern.compile(">");
033: private static final Pattern SINGLE_QUOTE = Pattern.compile("'");
034: private static final Pattern QUOTE = Pattern.compile("\"");
035: private static final Map<String, Pattern> ATTR_ESCAPES = new HashMap<String, Pattern>();
036:
037: static {
038: ATTR_ESCAPES.put("<", LESS);
039: ATTR_ESCAPES.put(">", GREATER);
040: ATTR_ESCAPES.put("'", SINGLE_QUOTE);
041: ATTR_ESCAPES.put(""", QUOTE);
042: }
043:
044: private String m_outputFileName = null;
045: private File m_outputFile = null;
046: private ITestContext m_testContext = null;
047:
048: /**
049: * keep lists of all the results
050: */
051: private int m_numPassed = 0;
052: private int m_numFailed = 0;
053: private int m_numSkipped = 0;
054: private int m_numFailedButIgnored = 0;
055: private List<ITestResult> m_allTests = Collections
056: .synchronizedList(new ArrayList<ITestResult>());
057: private List<ITestResult> m_configIssues = Collections
058: .synchronizedList(new ArrayList<ITestResult>());
059:
060: public void onTestStart(ITestResult result) {
061: }
062:
063: /**
064: * Invoked each time a test succeeds.
065: */
066: public void onTestSuccess(ITestResult tr) {
067: m_allTests.add(tr);
068: m_numPassed++;
069: }
070:
071: public void onTestFailedButWithinSuccessPercentage(ITestResult tr) {
072: m_allTests.add(tr);
073: m_numFailedButIgnored++;
074: }
075:
076: /**
077: * Invoked each time a test fails.
078: */
079: public void onTestFailure(ITestResult tr) {
080: m_allTests.add(tr);
081: m_numFailed++;
082: }
083:
084: /**
085: * Invoked each time a test is skipped.
086: */
087: public void onTestSkipped(ITestResult tr) {
088: m_allTests.add(tr);
089: m_numSkipped++;
090: }
091:
092: /**
093: * Invoked after the test class is instantiated and before
094: * any configuration method is called.
095: *
096: */
097: public void onStart(ITestContext context) {
098: m_outputFileName = context.getOutputDirectory()
099: + File.separator + context.getName() + ".xml";
100: m_outputFile = new File(m_outputFileName);
101: m_testContext = context;
102: }
103:
104: /**
105: * Invoked after all the tests have run and all their
106: * Configuration methods have been called.
107: *
108: */
109: public void onFinish(ITestContext context) {
110: generateReport();
111: }
112:
113: /**
114: * @see org.testng.internal.IConfigurationListener#onConfigurationFailure(org.testng.ITestResult)
115: */
116: public void onConfigurationFailure(ITestResult itr) {
117: m_configIssues.add(itr);
118: }
119:
120: /**
121: * @see org.testng.internal.IConfigurationListener#onConfigurationSkip(org.testng.ITestResult)
122: */
123: public void onConfigurationSkip(ITestResult itr) {
124: m_configIssues.add(itr);
125: }
126:
127: /**
128: * @see org.testng.internal.IConfigurationListener#onConfigurationSuccess(org.testng.ITestResult)
129: */
130: public void onConfigurationSuccess(ITestResult itr) {
131: }
132:
133: /**
134: * generate the XML report given what we know from all the test results
135: */
136: protected void generateReport() {
137: try {
138: XMLStringBuffer document = new XMLStringBuffer("");
139: document.setXmlDetails("1.0", "UTF-8");
140: Properties attrs = new Properties();
141: attrs.setProperty(XMLConstants.ATTR_NAME,
142: encodeAttr(m_testContext.getName())); // ENCODE
143: attrs.setProperty(XMLConstants.ATTR_TESTS, ""
144: + m_allTests.size());
145: attrs.setProperty(XMLConstants.ATTR_FAILURES, ""
146: + m_numFailed);
147: attrs.setProperty(XMLConstants.ATTR_ERRORS, "0");
148: attrs
149: .setProperty(
150: XMLConstants.ATTR_TIME,
151: ""
152: + ((m_testContext.getEndDate()
153: .getTime() - m_testContext
154: .getStartDate().getTime()) / 1000.0));
155:
156: document.push(XMLConstants.TESTSUITE, attrs);
157: document.addEmptyElement(XMLConstants.PROPERTIES);
158:
159: for (ITestResult tr : m_configIssues) {
160: createElement(document, tr);
161: }
162: for (ITestResult tr : m_allTests) {
163: createElement(document, tr);
164: }
165:
166: document.pop();
167: BufferedWriter fw = new BufferedWriter(new FileWriter(
168: m_outputFile));
169: fw.write(document.toXML());
170: fw.flush();
171: fw.close();
172: } catch (IOException ioe) {
173: ioe.printStackTrace();
174: System.err.println("failed to create JUnitXML because of "
175: + ioe);
176: }
177: }
178:
179: private void createElement(XMLStringBuffer doc, ITestResult tr) {
180: Properties attrs = new Properties();
181: long elapsedTimeMillis = tr.getEndMillis()
182: - tr.getStartMillis();
183: String name = tr.getMethod().isTest() ? tr.getName() : Utils
184: .detailedMethodName(tr.getMethod(), false);
185: attrs.setProperty(XMLConstants.ATTR_NAME, name);
186: attrs.setProperty(XMLConstants.ATTR_CLASSNAME, tr
187: .getTestClass().getRealClass().getName());
188: attrs.setProperty(XMLConstants.ATTR_TIME, ""
189: + (((double) elapsedTimeMillis) / 1000));
190:
191: if ((ITestResult.FAILURE == tr.getStatus())
192: || (ITestResult.SKIP == tr.getStatus())) {
193: doc.push(XMLConstants.TESTCASE, attrs);
194:
195: if (ITestResult.FAILURE == tr.getStatus()) {
196: createFailureElement(doc, tr);
197: } else if (ITestResult.SKIP == tr.getStatus()) {
198: createSkipElement(doc, tr);
199: }
200:
201: doc.pop();
202: } else {
203: doc.addEmptyElement(XMLConstants.TESTCASE, attrs);
204: }
205: }
206:
207: private void createFailureElement(XMLStringBuffer doc,
208: ITestResult tr) {
209: Properties attrs = new Properties();
210: Throwable t = tr.getThrowable();
211: if (t != null) {
212: attrs.setProperty(XMLConstants.ATTR_TYPE, t.getClass()
213: .getName());
214: String message = t.getMessage();
215: if ((message != null) && (message.length() > 0)) {
216: attrs.setProperty(XMLConstants.ATTR_MESSAGE,
217: encodeAttr(message)); // ENCODE
218: }
219: doc.push(XMLConstants.FAILURE, attrs);
220: doc.addCDATA(Utils.stackTrace(t, false)[0]);
221: doc.pop();
222: } else {
223: doc.addEmptyElement(XMLConstants.FAILURE); // THIS IS AN ERROR
224: }
225: }
226:
227: private void createSkipElement(XMLStringBuffer doc, ITestResult tr) {
228: doc.addEmptyElement("skipped");
229: }
230:
231: private String encodeAttr(String attr) {
232: String result = replaceAmpersand(attr, ENTITY);
233: for (Map.Entry<String, Pattern> e : ATTR_ESCAPES.entrySet()) {
234: result = e.getValue().matcher(result)
235: .replaceAll(e.getKey());
236: }
237:
238: return result;
239: }
240:
241: private String replaceAmpersand(String str, Pattern pattern) {
242: int start = 0;
243: int idx = str.indexOf('&', start);
244: if (idx == -1)
245: return str;
246: StringBuffer result = new StringBuffer();
247: while (idx != -1) {
248: result.append(str.substring(start, idx));
249: if (pattern.matcher(str.substring(idx)).matches()) {
250: // do nothing it is an entity;
251: result.append("&");
252: } else {
253: result.append("&");
254: }
255: start = idx + 1;
256: idx = str.indexOf('&', start);
257: }
258: result.append(str.substring(start));
259:
260: return result.toString();
261: }
262: }
|