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("<", LESS); 40 ATTR_ESCAPES.put(">", GREATER); 41 ATTR_ESCAPES.put("'", SINGLE_QUOTE); 42 ATTR_ESCAPES.put(""", 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("&"); 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