1. /*
  2. * Copyright 2001-2004 The Apache Software Foundation
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. *
  16. */
  17. package org.apache.tools.ant.taskdefs.optional.junit;
  18. import java.io.BufferedOutputStream;
  19. import java.io.File;
  20. import java.io.FileOutputStream;
  21. import java.io.IOException;
  22. import java.io.OutputStream;
  23. import java.io.OutputStreamWriter;
  24. import java.io.PrintWriter;
  25. import java.util.Enumeration;
  26. import java.util.Vector;
  27. import javax.xml.parsers.DocumentBuilder;
  28. import javax.xml.parsers.DocumentBuilderFactory;
  29. import org.apache.tools.ant.BuildException;
  30. import org.apache.tools.ant.DirectoryScanner;
  31. import org.apache.tools.ant.Project;
  32. import org.apache.tools.ant.Task;
  33. import org.apache.tools.ant.types.FileSet;
  34. import org.apache.tools.ant.util.DOMElementWriter;
  35. import org.apache.tools.ant.util.StringUtils;
  36. import org.w3c.dom.Document;
  37. import org.w3c.dom.Element;
  38. import org.xml.sax.SAXException;
  39. /**
  40. * Aggregates all <junit> XML formatter testsuite data under
  41. * a specific directory and transforms the results via XSLT.
  42. * It is not particulary clean but
  43. * should be helpful while I am thinking about another technique.
  44. *
  45. * <p> The main problem is due to the fact that a JVM can be forked for a testcase
  46. * thus making it impossible to aggregate all testcases since the listener is
  47. * (obviously) in the forked JVM. A solution could be to write a
  48. * TestListener that will receive events from the TestRunner via sockets. This
  49. * is IMHO the simplest way to do it to avoid this file hacking thing.
  50. *
  51. *
  52. * @ant.task name="junitreport" category="testing"
  53. */
  54. public class XMLResultAggregator extends Task implements XMLConstants {
  55. /** the list of all filesets, that should contains the xml to aggregate */
  56. protected Vector filesets = new Vector();
  57. /** the name of the result file */
  58. protected String toFile;
  59. /** the directory to write the file to */
  60. protected File toDir;
  61. protected Vector transformers = new Vector();
  62. /** The default directory: <tt>.</tt>. It is resolved from the project directory */
  63. public static final String DEFAULT_DIR = ".";
  64. /** the default file name: <tt>TESTS-TestSuites.xml</tt> */
  65. public static final String DEFAULT_FILENAME = "TESTS-TestSuites.xml";
  66. /**
  67. * Generate a report based on the document created by the merge.
  68. */
  69. public AggregateTransformer createReport() {
  70. AggregateTransformer transformer = new AggregateTransformer(this);
  71. transformers.addElement(transformer);
  72. return transformer;
  73. }
  74. /**
  75. * Set the name of the aggregegated results file. It must be relative
  76. * from the <tt>todir</tt> attribute. If not set it will use {@link #DEFAULT_FILENAME}
  77. * @param value the name of the file.
  78. * @see #setTodir(File)
  79. */
  80. public void setTofile(String value) {
  81. toFile = value;
  82. }
  83. /**
  84. * Set the destination directory where the results should be written. If not
  85. * set if will use {@link #DEFAULT_DIR}. When given a relative directory
  86. * it will resolve it from the project directory.
  87. * @param value the directory where to write the results, absolute or
  88. * relative.
  89. */
  90. public void setTodir(File value) {
  91. toDir = value;
  92. }
  93. /**
  94. * Add a new fileset containing the XML results to aggregate
  95. * @param fs the new fileset of xml results.
  96. */
  97. public void addFileSet(FileSet fs) {
  98. filesets.addElement(fs);
  99. }
  100. /**
  101. * Aggregate all testsuites into a single document and write it to the
  102. * specified directory and file.
  103. * @throws BuildException thrown if there is a serious error while writing
  104. * the document.
  105. */
  106. public void execute() throws BuildException {
  107. Element rootElement = createDocument();
  108. File destFile = getDestinationFile();
  109. // write the document
  110. try {
  111. writeDOMTree(rootElement.getOwnerDocument(), destFile);
  112. } catch (IOException e) {
  113. throw new BuildException("Unable to write test aggregate to '" + destFile + "'", e);
  114. }
  115. // apply transformation
  116. Enumeration e = transformers.elements();
  117. while (e.hasMoreElements()) {
  118. AggregateTransformer transformer =
  119. (AggregateTransformer) e.nextElement();
  120. transformer.setXmlDocument(rootElement.getOwnerDocument());
  121. transformer.transform();
  122. }
  123. }
  124. /**
  125. * Get the full destination file where to write the result. It is made of
  126. * the <tt>todir</tt> and <tt>tofile</tt> attributes.
  127. * @return the destination file where should be written the result file.
  128. */
  129. protected File getDestinationFile() {
  130. if (toFile == null) {
  131. toFile = DEFAULT_FILENAME;
  132. }
  133. if (toDir == null) {
  134. toDir = getProject().resolveFile(DEFAULT_DIR);
  135. }
  136. return new File(toDir, toFile);
  137. }
  138. /**
  139. * Get all <code>.xml</code> files in the fileset.
  140. *
  141. * @return all files in the fileset that end with a '.xml'.
  142. */
  143. protected File[] getFiles() {
  144. Vector v = new Vector();
  145. final int size = filesets.size();
  146. for (int i = 0; i < size; i++) {
  147. FileSet fs = (FileSet) filesets.elementAt(i);
  148. DirectoryScanner ds = fs.getDirectoryScanner(getProject());
  149. ds.scan();
  150. String[] f = ds.getIncludedFiles();
  151. for (int j = 0; j < f.length; j++) {
  152. String pathname = f[j];
  153. if (pathname.endsWith(".xml")) {
  154. File file = new File(ds.getBasedir(), pathname);
  155. file = getProject().resolveFile(file.getPath());
  156. v.addElement(file);
  157. }
  158. }
  159. }
  160. File[] files = new File[v.size()];
  161. v.copyInto(files);
  162. return files;
  163. }
  164. //----- from now, the methods are all related to DOM tree manipulation
  165. /**
  166. * Write the DOM tree to a file.
  167. * @param doc the XML document to dump to disk.
  168. * @param file the filename to write the document to. Should obviouslly be a .xml file.
  169. * @throws IOException thrown if there is an error while writing the content.
  170. */
  171. protected void writeDOMTree(Document doc, File file) throws IOException {
  172. OutputStream out = null;
  173. PrintWriter wri = null;
  174. try {
  175. out = new BufferedOutputStream(new FileOutputStream(file));
  176. wri = new PrintWriter(new OutputStreamWriter(out, "UTF8"));
  177. wri.write("<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n");
  178. (new DOMElementWriter()).write(doc.getDocumentElement(), wri, 0, " ");
  179. wri.flush();
  180. // writers do not throw exceptions, so check for them.
  181. if (wri.checkError()) {
  182. throw new IOException("Error while writing DOM content");
  183. }
  184. } finally {
  185. if (wri != null) {
  186. wri.close();
  187. out = null;
  188. }
  189. if (out != null) {
  190. out.close();
  191. }
  192. }
  193. }
  194. /**
  195. * <p> Create a DOM tree.
  196. * Has 'testsuites' as firstchild and aggregates all
  197. * testsuite results that exists in the base directory.
  198. * @return the root element of DOM tree that aggregates all testsuites.
  199. */
  200. protected Element createDocument() {
  201. // create the dom tree
  202. DocumentBuilder builder = getDocumentBuilder();
  203. Document doc = builder.newDocument();
  204. Element rootElement = doc.createElement(TESTSUITES);
  205. doc.appendChild(rootElement);
  206. // get all files and add them to the document
  207. File[] files = getFiles();
  208. for (int i = 0; i < files.length; i++) {
  209. try {
  210. log("Parsing file: '" + files[i] + "'", Project.MSG_VERBOSE);
  211. //XXX there seems to be a bug in xerces 1.3.0 that doesn't like file object
  212. // will investigate later. It does not use the given directory but
  213. // the vm dir instead ? Works fine with crimson.
  214. Document testsuiteDoc
  215. = builder.parse("file:///" + files[i].getAbsolutePath());
  216. Element elem = testsuiteDoc.getDocumentElement();
  217. // make sure that this is REALLY a testsuite.
  218. if (TESTSUITE.equals(elem.getNodeName())) {
  219. addTestSuite(rootElement, elem);
  220. } else {
  221. // issue a warning.
  222. log("the file " + files[i]
  223. + " is not a valid testsuite XML document",
  224. Project.MSG_WARN);
  225. }
  226. } catch (SAXException e) {
  227. // a testcase might have failed and write a zero-length document,
  228. // It has already failed, but hey.... mm. just put a warning
  229. log("The file " + files[i] + " is not a valid XML document. "
  230. + "It is possibly corrupted.", Project.MSG_WARN);
  231. log(StringUtils.getStackTrace(e), Project.MSG_DEBUG);
  232. } catch (IOException e) {
  233. log("Error while accessing file " + files[i] + ": "
  234. + e.getMessage(), Project.MSG_ERR);
  235. }
  236. }
  237. return rootElement;
  238. }
  239. /**
  240. * <p> Add a new testsuite node to the document.
  241. * The main difference is that it
  242. * split the previous fully qualified name into a package and a name.
  243. * <p> For example: <tt>org.apache.Whatever</tt> will be split into
  244. * <tt>org.apache</tt> and <tt>Whatever</tt>.
  245. * @param root the root element to which the <tt>testsuite</tt> node should
  246. * be appended.
  247. * @param testsuite the element to append to the given root. It will slightly
  248. * modify the original node to change the name attribute and add
  249. * a package one.
  250. */
  251. protected void addTestSuite(Element root, Element testsuite) {
  252. String fullclassname = testsuite.getAttribute(ATTR_NAME);
  253. int pos = fullclassname.lastIndexOf('.');
  254. // a missing . might imply no package at all. Don't get fooled.
  255. String pkgName = (pos == -1) ? "" : fullclassname.substring(0, pos);
  256. String classname = (pos == -1) ? fullclassname : fullclassname.substring(pos + 1);
  257. Element copy = (Element) DOMUtil.importNode(root, testsuite);
  258. // modify the name attribute and set the package
  259. copy.setAttribute(ATTR_NAME, classname);
  260. copy.setAttribute(ATTR_PACKAGE, pkgName);
  261. }
  262. /**
  263. * Create a new document builder. Will issue an <tt>ExceptionInitializerError</tt>
  264. * if something is going wrong. It is fatal anyway.
  265. * @todo factorize this somewhere else. It is duplicated code.
  266. * @return a new document builder to create a DOM
  267. */
  268. private static DocumentBuilder getDocumentBuilder() {
  269. try {
  270. return DocumentBuilderFactory.newInstance().newDocumentBuilder();
  271. } catch (Exception exc) {
  272. throw new ExceptionInInitializerError(exc);
  273. }
  274. }
  275. }