1package org.testng.reporters;
2
3import org.testng.IReporter;
4import org.testng.ISuite;
5import org.testng.ISuiteResult;
6import org.testng.ITestContext;
7import org.testng.ITestNGMethod;
8import org.testng.ITestResult;
9import org.testng.collections.ListMultiMap;
10import org.testng.collections.Lists;
11import org.testng.collections.Maps;
12import org.testng.collections.Sets;
13import org.testng.internal.Utils;
14import org.testng.xml.XmlSuite;
15
16import java.io.File;
17import java.io.PrintWriter;
18import java.io.StringWriter;
19import java.net.InetAddress;
20import java.net.UnknownHostException;
21import java.text.DecimalFormat;
22import java.text.DecimalFormatSymbols;
23import java.util.Calendar;
24import java.util.Date;
25import java.util.List;
26import java.util.Map;
27import java.util.Properties;
28import java.util.Set;
29
30public class JUnitReportReporter implements IReporter {
31
32  @Override
33  public void generateReport(List<XmlSuite> xmlSuites, List<ISuite> suites,
34      String defaultOutputDirectory) {
35
36    Map<Class<?>, Set<ITestResult>> results = Maps.newHashMap();
37    Map<Class<?>, Set<ITestResult>> failedConfigurations = Maps.newHashMap();
38    ListMultiMap<Object, ITestResult> befores = Maps.newListMultiMap();
39    ListMultiMap<Object, ITestResult> afters = Maps.newListMultiMap();
40    for (ISuite suite : suites) {
41      Map<String, ISuiteResult> suiteResults = suite.getResults();
42      for (ISuiteResult sr : suiteResults.values()) {
43        ITestContext tc = sr.getTestContext();
44        addResults(tc.getPassedTests().getAllResults(), results);
45        addResults(tc.getFailedTests().getAllResults(), results);
46        addResults(tc.getSkippedTests().getAllResults(), results);
47        addResults(tc.getFailedConfigurations().getAllResults(), failedConfigurations);
48        for (ITestResult tr : tc.getPassedConfigurations().getAllResults()) {
49          if (tr.getMethod().isBeforeMethodConfiguration()) {
50            befores.put(tr.getInstance(), tr);
51          }
52          if (tr.getMethod().isAfterMethodConfiguration()) {
53            afters.put(tr.getInstance(), tr);
54          }
55        }
56      }
57    }
58
59    // A list of iterators for all the passed configuration, explanation below
60//    ListMultiMap<Class<?>, ITestResult> beforeConfigurations = Maps.newListMultiMap();
61//    ListMultiMap<Class<?>, ITestResult> afterConfigurations = Maps.newListMultiMap();
62//    for (Map.Entry<Class<?>, Set<ITestResult>> es : passedConfigurations.entrySet()) {
63//      for (ITestResult tr : es.getValue()) {
64//        ITestNGMethod method = tr.getMethod();
65//        if (method.isBeforeMethodConfiguration()) {
66//          beforeConfigurations.put(method.getRealClass(), tr);
67//        }
68//        if (method.isAfterMethodConfiguration()) {
69//          afterConfigurations.put(method.getRealClass(), tr);
70//        }
71//      }
72//    }
73//    Map<Object, Iterator<ITestResult>> befores = Maps.newHashMap();
74//    for (Map.Entry<Class<?>, List<ITestResult>> es : beforeConfigurations.getEntrySet()) {
75//      List<ITestResult> tr = es.getValue();
76//      for (ITestResult itr : es.getValue()) {
77//      }
78//    }
79//    Map<Class<?>, Iterator<ITestResult>> afters = Maps.newHashMap();
80//    for (Map.Entry<Class<?>, List<ITestResult>> es : afterConfigurations.getEntrySet()) {
81//      afters.put(es.getKey(), es.getValue().iterator());
82//    }
83
84    for (Map.Entry<Class<?>, Set<ITestResult>> entry : results.entrySet()) {
85      Class<?> cls = entry.getKey();
86      Properties p1 = new Properties();
87      p1.setProperty("name", cls.getName());
88      Date timeStamp = Calendar.getInstance().getTime();
89      p1.setProperty(XMLConstants.ATTR_TIMESTAMP, timeStamp.toGMTString());
90
91      List<TestTag> testCases = Lists.newArrayList();
92      int failures = 0;
93      int errors = 0;
94      int testCount = 0;
95      float totalTime = 0;
96
97      for (ITestResult tr: entry.getValue()) {
98        TestTag testTag = new TestTag();
99
100        boolean isSuccess = tr.getStatus() == ITestResult.SUCCESS;
101        if (! isSuccess) {
102          if (tr.getThrowable() instanceof AssertionError) {
103            failures++;
104          } else {
105            errors++;
106          }
107        }
108
109        Properties p2 = new Properties();
110        p2.setProperty("classname", cls.getName());
111        p2.setProperty("name", getTestName(tr));
112        long time = tr.getEndMillis() - tr.getStartMillis();
113
114        time += getNextConfiguration(befores, tr);
115        time += getNextConfiguration(afters, tr);
116
117        p2.setProperty("time", "" + formatTime(time));
118        Throwable t = getThrowable(tr, failedConfigurations);
119        if (! isSuccess && t != null) {
120          StringWriter sw = new StringWriter();
121          PrintWriter pw = new PrintWriter(sw);
122          t.printStackTrace(pw);
123          testTag.message = t.getMessage();
124          testTag.type = t.getClass().getName();
125          testTag.stackTrace = sw.toString();
126          testTag.errorTag = tr.getThrowable() instanceof AssertionError ? "failure" : "error";
127        }
128        totalTime += time;
129        testCount++;
130        testTag.properties = p2;
131        testCases.add(testTag);
132      }
133
134      p1.setProperty("failures", "" + failures);
135      p1.setProperty("errors", "" + errors);
136      p1.setProperty("name", cls.getName());
137      p1.setProperty("tests", "" + testCount);
138      p1.setProperty("time", "" + formatTime(totalTime));
139      try {
140        p1.setProperty(XMLConstants.ATTR_HOSTNAME, InetAddress.getLocalHost().getHostName());
141      } catch (UnknownHostException e) {
142        // ignore
143      }
144
145      //
146      // Now that we have all the information we need, generate the file
147      //
148      XMLStringBuffer xsb = new XMLStringBuffer();
149      xsb.addComment("Generated by " + getClass().getName());
150
151      xsb.push("testsuite", p1);
152      for (TestTag testTag : testCases) {
153        if (testTag.stackTrace == null) {
154          xsb.addEmptyElement("testcase", testTag.properties);
155        }
156        else {
157          xsb.push("testcase", testTag.properties);
158
159          Properties p = new Properties();
160          if (testTag.message != null) {
161            p.setProperty("message", testTag.message);
162          }
163          p.setProperty("type", testTag.type);
164          xsb.push(testTag.errorTag, p);
165          xsb.addCDATA(testTag.stackTrace);
166          xsb.pop(testTag.errorTag);
167
168          xsb.pop("testcase");
169        }
170      }
171      xsb.pop("testsuite");
172
173      String outputDirectory = defaultOutputDirectory + File.separator + "junitreports";
174      Utils.writeUtf8File(outputDirectory, getFileName(cls), xsb.toXML());
175    }
176
177//    System.out.println(xsb.toXML());
178//    System.out.println("");
179
180  }
181
182  /**
183   * Add the time of the configuration method to this test method.
184   *
185   * The only problem with this method is that the timing of a test method
186   * might not be added to the time of the same configuration method that ran before
187   * it but since they should all be equivalent, this should never be an issue.
188   */
189  private long getNextConfiguration(ListMultiMap<Object, ITestResult> configurations,
190      ITestResult tr)
191  {
192    long result = 0;
193
194    List<ITestResult> confResults = configurations.get(tr.getInstance());
195    Map<ITestNGMethod, ITestResult> seen = Maps.newHashMap();
196    if (confResults != null) {
197      for (ITestResult r : confResults) {
198        if (! seen.containsKey(r.getMethod())) {
199          result += r.getEndMillis() - r.getStartMillis();
200          seen.put(r.getMethod(), r);
201        }
202      }
203      confResults.removeAll(seen.values());
204    }
205
206    return result;
207  }
208
209  protected String getFileName(Class cls) {
210    return "TEST-" + cls.getName() + ".xml";
211  }
212
213  protected String getTestName(ITestResult tr) {
214    return tr.getMethod().getMethodName();
215  }
216
217  private String formatTime(float time) {
218    DecimalFormatSymbols symbols = new DecimalFormatSymbols();
219    // JUnitReports wants points here, regardless of the locale
220    symbols.setDecimalSeparator('.');
221    DecimalFormat format = new DecimalFormat("#.###", symbols);
222    format.setMinimumFractionDigits(3);
223    return format.format(time / 1000.0f);
224  }
225
226  private Throwable getThrowable(ITestResult tr,
227      Map<Class<?>, Set<ITestResult>> failedConfigurations) {
228    Throwable result = tr.getThrowable();
229    if (result == null && tr.getStatus() == ITestResult.SKIP) {
230      // Attempt to grab the stack trace from the configuration failure
231      for (Set<ITestResult> failures : failedConfigurations.values()) {
232        for (ITestResult failure : failures) {
233          // Naive implementation for now, eventually, we need to try to find
234          // out if it's this failure that caused the skip since (maybe by
235          // seeing if the class of the configuration method is assignable to
236          // the class of the test method, although that's not 100% fool proof
237          if (failure.getThrowable() != null) {
238            return failure.getThrowable();
239          }
240        }
241      }
242    }
243
244    return result;
245  }
246
247  static class TestTag {
248    public Properties properties;
249    public String message;
250    public String type;
251    public String stackTrace;
252    public String errorTag;
253  }
254
255  private void addResults(Set<ITestResult> allResults, Map<Class<?>, Set<ITestResult>> out) {
256    for (ITestResult tr : allResults) {
257      Class<?> cls = tr.getMethod().getTestClass().getRealClass();
258      Set<ITestResult> l = out.get(cls);
259      if (l == null) {
260        l = Sets.newHashSet();
261        out.put(cls, l);
262      }
263      l.add(tr);
264    }
265  }
266
267}
268