1 package org.testng.reporters;
2 
3 
4 import org.testng.ITestContext;
5 import org.testng.ITestNGMethod;
6 import org.testng.ITestResult;
7 import org.testng.collections.Lists;
8 import org.testng.collections.Maps;
9 import org.testng.collections.Sets;
10 import org.testng.internal.IResultListener2;
11 import org.testng.internal.Utils;
12 
13 import java.net.InetAddress;
14 import java.net.UnknownHostException;
15 import java.util.Calendar;
16 import java.util.Collections;
17 import java.util.Date;
18 import java.util.List;
19 import java.util.Map;
20 import java.util.Properties;
21 import java.util.Set;
22 import java.util.regex.Pattern;
23 
24 /**
25  * A JUnit XML report generator (replacing the original JUnitXMLReporter that was
26  * based on XML APIs).
27  *
28  * @author <a href='mailto:the[dot]mindstorm[at]gmail[dot]com'>Alex Popescu</a>
29  */
30 public class JUnitXMLReporter implements IResultListener2 {
31   private static final Pattern ENTITY= Pattern.compile("&[a-zA-Z]+;.*");
32   private static final Pattern LESS= Pattern.compile("<");
33   private static final Pattern GREATER= Pattern.compile(">");
34   private static final Pattern SINGLE_QUOTE = Pattern.compile("'");
35   private static final Pattern QUOTE = Pattern.compile("\"");
36   private static final Map<String, Pattern> ATTR_ESCAPES= Maps.newHashMap();
37 
38   static {
39     ATTR_ESCAPES.put("&lt;", LESS);
40     ATTR_ESCAPES.put("&gt;", GREATER);
41     ATTR_ESCAPES.put("&apos;", SINGLE_QUOTE);
42     ATTR_ESCAPES.put("&quot;", QUOTE);
43   }
44 
45 
46   /**
47    * keep lists of all the results
48    */
49   private int m_numPassed= 0;
50   private int m_numFailed= 0;
51   private int m_numSkipped= 0;
52   private int m_numFailedButIgnored= 0;
53   private List<ITestResult> m_allTests =
54       Collections.synchronizedList(Lists.<ITestResult>newArrayList());
55   private List<ITestResult> m_configIssues =
56       Collections.synchronizedList(Lists.<ITestResult>newArrayList());
57   private Map<String, String> m_fileNameMap = Maps.newHashMap();
58   private int m_fileNameIncrementer = 0;
59 
60   @Override
onTestStart(ITestResult result)61   public void onTestStart(ITestResult result) {
62   }
63 
64   @Override
beforeConfiguration(ITestResult tr)65   public void beforeConfiguration(ITestResult tr) {
66   }
67 
68   /**
69    * Invoked each time a test succeeds.
70    */
71   @Override
onTestSuccess(ITestResult tr)72   public void onTestSuccess(ITestResult tr) {
73     m_allTests.add(tr);
74     m_numPassed++;
75   }
76 
77   @Override
onTestFailedButWithinSuccessPercentage(ITestResult tr)78   public void onTestFailedButWithinSuccessPercentage(ITestResult tr) {
79     m_allTests.add(tr);
80     m_numFailedButIgnored++;
81   }
82 
83   /**
84    * Invoked each time a test fails.
85    */
86   @Override
onTestFailure(ITestResult tr)87   public void onTestFailure(ITestResult tr) {
88     m_allTests.add(tr);
89     m_numFailed++;
90   }
91 
92   /**
93    * Invoked each time a test is skipped.
94    */
95   @Override
onTestSkipped(ITestResult tr)96   public void onTestSkipped(ITestResult tr) {
97     m_allTests.add(tr);
98     m_numSkipped++;
99   }
100 
101   /**
102    * Invoked after the test class is instantiated and before
103    * any configuration method is called.
104    *
105    */
106   @Override
onStart(ITestContext context)107   public void onStart(ITestContext context) {
108 
109   }
110 
111   /**
112    * Invoked after all the tests have run and all their
113    * Configuration methods have been called.
114    *
115    */
116   @Override
onFinish(ITestContext context)117   public void onFinish(ITestContext context) {
118 	generateReport(context);
119     resetAll();
120   }
121 
122   /**
123    * @see org.testng.IConfigurationListener#onConfigurationFailure(org.testng.ITestResult)
124    */
125   @Override
onConfigurationFailure(ITestResult itr)126   public void onConfigurationFailure(ITestResult itr) {
127     m_configIssues.add(itr);
128   }
129 
130   /**
131    * @see org.testng.IConfigurationListener#onConfigurationSkip(org.testng.ITestResult)
132    */
133   @Override
onConfigurationSkip(ITestResult itr)134   public void onConfigurationSkip(ITestResult itr) {
135     m_configIssues.add(itr);
136   }
137 
138   /**
139    * @see org.testng.IConfigurationListener#onConfigurationSuccess(org.testng.ITestResult)
140    */
141   @Override
onConfigurationSuccess(ITestResult itr)142   public void onConfigurationSuccess(ITestResult itr) {
143   }
144 
145   /**
146    * generate the XML report given what we know from all the test results
147    */
generateReport(ITestContext context)148   protected void generateReport(ITestContext context) {
149 
150       XMLStringBuffer document= new XMLStringBuffer();
151       document.addComment("Generated by " + getClass().getName());
152 
153       Properties attrs= new Properties();
154       attrs.setProperty(XMLConstants.ATTR_ERRORS, "0");
155       attrs.setProperty(XMLConstants.ATTR_FAILURES, "" + m_numFailed);
156       try {
157         attrs.setProperty(XMLConstants.ATTR_HOSTNAME, InetAddress.getLocalHost().getHostName());
158       } catch (UnknownHostException e) {
159         // ignore
160       }
161       Set<String> packages = getPackages(context);
162       if (packages.size() > 0) {
163         attrs.setProperty(XMLConstants.ATTR_NAME, context.getCurrentXmlTest().getName());
164 //        attrs.setProperty(XMLConstants.ATTR_PACKAGE, packages.iterator().next());
165       }
166 
167       attrs.setProperty(XMLConstants.ATTR_TESTS, "" + m_allTests.size());
168       attrs.setProperty(XMLConstants.ATTR_TIME, ""
169           + ((context.getEndDate().getTime() - context.getStartDate().getTime()) / 1000.0));
170 
171       Date timeStamp = Calendar.getInstance().getTime();
172       attrs.setProperty(XMLConstants.ATTR_TIMESTAMP, timeStamp.toGMTString());
173 
174       document.push(XMLConstants.TESTSUITE, attrs);
175 //      document.addEmptyElement(XMLConstants.PROPERTIES);
176 
177       createElementFromTestResults(document, m_configIssues);
178       createElementFromTestResults(document, m_allTests);
179 
180       document.pop();
181       Utils.writeUtf8File(context.getOutputDirectory(),generateFileName(context) + ".xml", document.toXML());
182   }
183 
createElementFromTestResults(XMLStringBuffer document, List<ITestResult> results)184   private void createElementFromTestResults(XMLStringBuffer document, List<ITestResult> results) {
185     synchronized(results) {
186       for(ITestResult tr : results) {
187         createElement(document, tr);
188       }
189     }
190   }
191 
getPackages(ITestContext context)192   private Set<String> getPackages(ITestContext context) {
193     Set<String> result = Sets.newHashSet();
194     for (ITestNGMethod m : context.getAllTestMethods()) {
195       Package pkg = m.getMethod().getDeclaringClass().getPackage();
196       if (pkg != null) {
197         result.add(pkg.getName());
198       }
199     }
200     return result;
201   }
202 
createElement(XMLStringBuffer doc, ITestResult tr)203   private void createElement(XMLStringBuffer doc, ITestResult tr) {
204     Properties attrs= new Properties();
205     long elapsedTimeMillis= tr.getEndMillis() - tr.getStartMillis();
206     String name= tr.getMethod().isTest() ? tr.getName() : Utils.detailedMethodName(tr.getMethod(), false);
207     attrs.setProperty(XMLConstants.ATTR_NAME, name);
208     attrs.setProperty(XMLConstants.ATTR_CLASSNAME, tr.getTestClass().getRealClass().getName());
209     attrs.setProperty(XMLConstants.ATTR_TIME, "" + (((double) elapsedTimeMillis) / 1000));
210 
211     if((ITestResult.FAILURE == tr.getStatus()) || (ITestResult.SKIP == tr.getStatus())) {
212       doc.push(XMLConstants.TESTCASE, attrs);
213 
214       if(ITestResult.FAILURE == tr.getStatus()) {
215         createFailureElement(doc, tr);
216       }
217       else if(ITestResult.SKIP == tr.getStatus()) {
218         createSkipElement(doc, tr);
219       }
220 
221       doc.pop();
222     }
223     else {
224       doc.addEmptyElement(XMLConstants.TESTCASE, attrs);
225     }
226   }
227 
createFailureElement(XMLStringBuffer doc, ITestResult tr)228   private void createFailureElement(XMLStringBuffer doc, ITestResult tr) {
229     Properties attrs= new Properties();
230     Throwable t= tr.getThrowable();
231     if(t != null) {
232       attrs.setProperty(XMLConstants.ATTR_TYPE, t.getClass().getName());
233       String message= t.getMessage();
234       if((message != null) && (message.length() > 0)) {
235         attrs.setProperty(XMLConstants.ATTR_MESSAGE, encodeAttr(message)); // ENCODE
236       }
237       doc.push(XMLConstants.FAILURE, attrs);
238       doc.addCDATA(Utils.stackTrace(t, false)[0]);
239       doc.pop();
240     }
241     else {
242       doc.addEmptyElement(XMLConstants.FAILURE); // THIS IS AN ERROR
243     }
244   }
245 
createSkipElement(XMLStringBuffer doc, ITestResult tr)246   private void createSkipElement(XMLStringBuffer doc, ITestResult tr) {
247     doc.addEmptyElement("skipped");
248   }
249 
encodeAttr(String attr)250   private String encodeAttr(String attr) {
251     String result= replaceAmpersand(attr, ENTITY);
252     for(Map.Entry<String, Pattern> e: ATTR_ESCAPES.entrySet()) {
253       result= e.getValue().matcher(result).replaceAll(e.getKey());
254     }
255 
256     return result;
257   }
258 
replaceAmpersand(String str, Pattern pattern)259   private String replaceAmpersand(String str, Pattern pattern) {
260     int start = 0;
261     int idx = str.indexOf('&', start);
262     if(idx == -1) {
263       return str;
264     }
265     StringBuffer result= new StringBuffer();
266     while(idx != -1) {
267       result.append(str.substring(start, idx));
268       if(pattern.matcher(str.substring(idx)).matches()) {
269         // do nothing it is an entity;
270         result.append("&");
271       }
272       else {
273         result.append("&amp;");
274       }
275       start= idx + 1;
276       idx= str.indexOf('&', start);
277     }
278     result.append(str.substring(start));
279 
280     return result.toString();
281   }
282 
283 
284   /**
285 	 * Reset all member variables for next test.
286 	 * */
resetAll()287 	private void resetAll() {
288 		m_allTests = Collections.synchronizedList(Lists.<ITestResult>newArrayList());
289 		m_configIssues = Collections.synchronizedList(Lists.<ITestResult>newArrayList());
290 		m_numFailed = 0;
291 		m_numFailedButIgnored = 0;
292 		m_numPassed = 0;
293 		m_numSkipped = 0;
294 	}
295 
296 	/**
297 	 * @author Borojevic Created this method to guarantee unique file names for
298 	 *         reports.<br>
299 	 *         Also, this will guarantee that the old reports are overwritten
300 	 *         when tests are run again.
301 	 * @param context
302 	 *            test context
303 	 * @return unique name for the file associated with this test context.
304 	 * */
generateFileName(ITestContext context)305 	private String generateFileName(ITestContext context) {
306 		String fileName = null;
307 		String keyToSearch = context.getSuite().getName() + context.getName();
308 		if (m_fileNameMap.get(keyToSearch) == null) {
309 			fileName = context.getName();
310 		} else {
311 			fileName = context.getName() + m_fileNameIncrementer++;
312 		}
313 
314 		m_fileNameMap.put(keyToSearch, fileName);
315 		return fileName;
316 	}
317 }
318