View Javadoc

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