001    /**
002     *  Licensed to the Apache Software Foundation (ASF) under one
003     *  or more contributor license agreements.  See the NOTICE file
004     *  distributed with this work for additional information
005     *  regarding copyright ownership.  The ASF licenses this file
006     *  to you under the Apache License, Version 2.0 (the
007     *  "License"); you may not use this file except in compliance
008     *  with the License.  You may obtain a copy of the License at
009     *
010     *    http://www.apache.org/licenses/LICENSE-2.0
011     *
012     *  Unless required by applicable law or agreed to in writing,
013     *  software distributed under the License is distributed on an
014     *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015     *  KIND, either express or implied.  See the License for the
016     *  specific language governing permissions and limitations
017     *  under the License.
018     */
019    
020    package org.apache.geronimo.mavenplugins.geronimo.reporting;
021    
022    import java.io.BufferedInputStream;
023    import java.io.BufferedWriter;
024    import java.io.File;
025    import java.io.FileInputStream;
026    import java.io.FileNotFoundException;
027    import java.io.FileOutputStream;
028    import java.io.IOException;
029    import java.io.InputStream;
030    import java.io.OutputStreamWriter;
031    import java.io.PrintWriter;
032    import java.io.UnsupportedEncodingException;
033    import java.io.FileWriter;
034    import java.text.NumberFormat;
035    import java.util.ArrayList;
036    import java.util.Collections;
037    import java.util.Enumeration;
038    import java.util.Iterator;
039    import java.util.List;
040    import java.util.Locale;
041    import java.util.Properties;
042    
043    import org.apache.commons.logging.Log;
044    import org.apache.commons.logging.LogFactory;
045    
046    import org.apache.maven.surefire.report.ReporterException;
047    import org.apache.maven.surefire.util.PrettyPrintXMLWriter;
048    
049    import org.codehaus.plexus.util.FileUtils;
050    import org.codehaus.plexus.util.IOUtil;
051    import org.codehaus.plexus.util.StringUtils;
052    import org.codehaus.plexus.util.xml.Xpp3Dom;
053    import org.codehaus.plexus.util.xml.Xpp3DomWriter;
054    
055    /**
056     * A reporter that generates Surefire result data, so the Surefire report can be used.
057     *
058     * @version $Rev: 476061 $ $Date: 2006-11-17 01:36:50 -0500 (Fri, 17 Nov 2006) $
059     */
060    public class SurefireReporter
061        implements Reporter
062    {
063        private static final Log log = LogFactory.getLog(SurefireReporter.class);
064    
065        private static final String LS = System.getProperty("line.separator");
066    
067        /**
068         * The name of the test (goal name).
069         */
070        private String testName;
071    
072        /**
073         * The input log file, may or may not exist.
074         */
075        private File logFile;
076    
077        /**
078         * The failure cause.
079         */
080        private Throwable failureCause;
081    
082        /**
083         * @parameter expression="${project.build.directory}/surefire-reports"
084         */
085        private File reportsDirectory = null;
086        
087        /**
088         * The file where the test output text will be written.
089         */
090        private File outputFile;
091    
092        /**
093         * The file where the text result xml will be written.
094         */
095        private File reportFile;
096    
097        /**
098         * The time when the test started.
099         */
100        private long startTime;
101    
102        /**
103         * The number of errors.
104         */
105        private int numErrors = 0;
106    
107        private static final int MS_PER_SEC = 1000;
108    
109        private NumberFormat numberFormat = NumberFormat.getInstance(Locale.ENGLISH);
110    
111        private List results = Collections.synchronizedList(new ArrayList());
112    
113        //
114        // Reporter
115        //
116    
117        public void reportBegin(final Reportable source) {
118            assert source != null;
119    
120            logFile = source.getLogFile();
121            testName = source.getName();
122            startTime = source.getStartTime().getTime();
123    
124            try {
125                FileUtils.forceMkdir(reportsDirectory);
126            }
127            catch (IOException e) {
128                //
129                // HACK: Maybe need to add a throws to beginReport() ?
130                //
131                
132                log.error("Failed to make reports directory: " + reportsDirectory, e);
133            }
134    
135            outputFile = new File(reportsDirectory, testName + ".txt");
136            reportFile = new File(reportsDirectory, "TEST-" + testName + ".xml");
137            if (reportFile.exists()) {
138                reportFile.delete();
139            }
140        }
141    
142        public void reportError(final Throwable cause) {
143            assert cause != null;
144    
145            log.debug("Capturing failed report from cause", cause);
146            
147            this.failureCause = cause;
148        }
149    
150        public void reportEnd() {
151            try {
152                if (logFile.exists()) {
153                    FileUtils.copyFile(logFile, outputFile);
154                }
155            }
156            catch (Exception e) {
157                log.warn("Failed to update outputFile", e);
158            }
159            
160            if (failureCause != null) {
161                try {
162                    boolean append = outputFile.exists();
163                    PrintWriter writer = new PrintWriter(new BufferedWriter(new FileWriter(outputFile, append)));
164                    try {
165                        writer.println("Exception Detail");
166                        writer.println("================");
167                        failureCause.printStackTrace(writer);
168                        writer.flush();
169                    }
170                    finally {
171                        writer.close();
172                    }
173                }
174                catch (Exception e) {
175                    log.warn("Failed to append error detail to outputFile", e);    
176                }
177    
178                testFailed();
179            }
180            else {
181                testSucceeded();
182            }
183    
184            try {
185                testSetCompleted();
186            }
187            catch (ReporterException e) {
188                log.warn("Failed to set test completed", e);
189            }
190        }
191    
192        //
193        // Surefire Support
194        //
195    
196        private void testSetCompleted() throws ReporterException {
197            long runTime = System.currentTimeMillis() - this.startTime;
198    
199            Xpp3Dom testSuite = createTestElement("testsuite", testName, runTime);
200    
201            showProperties(testSuite);
202    
203            testSuite.setAttribute("tests", "1");
204            testSuite.setAttribute("errors", String.valueOf(numErrors));
205            testSuite.setAttribute("skipped", "0");
206            testSuite.setAttribute("failures", "0");
207    
208            for (Iterator i = results.iterator(); i.hasNext();) {
209                Xpp3Dom testcase = (Xpp3Dom) i.next();
210                testSuite.addChild(testcase);
211            }
212    
213            PrintWriter writer = null;
214    
215            try {
216                writer = new PrintWriter(new BufferedWriter(new OutputStreamWriter(new FileOutputStream(reportFile), "UTF-8")));
217                writer.write("<?xml version=\"1.0\" encoding=\"UTF-8\" ?>" + LS);
218                Xpp3DomWriter.write(new PrettyPrintXMLWriter(writer), testSuite);
219            }
220            catch (UnsupportedEncodingException e) {
221                throw new ReporterException("Unable to use UTF-8 encoding", e);
222            }
223            catch (FileNotFoundException e) {
224                throw new ReporterException("Unable to create file: " + e.getMessage(), e);
225            }
226            finally {
227                IOUtil.close(writer);
228            }
229        }
230    
231        private Xpp3Dom createTestElement(String element, String testName, long runTime) {
232            Xpp3Dom testCase = new Xpp3Dom(element);
233            testCase.setAttribute("name", testName);
234            testCase.setAttribute("time", elapsedTimeAsString(runTime));
235    
236            return testCase;
237        }
238    
239        private Xpp3Dom createElement(Xpp3Dom element, String testName) {
240            Xpp3Dom component = new Xpp3Dom(testName);
241            element.addChild(component);
242    
243            return component;
244        }
245    
246        private void testSucceeded() {
247            long runTime = System.currentTimeMillis() - this.startTime;
248            Xpp3Dom testCase = createTestElement("testcase", testName, runTime);
249            results.add(testCase);
250        }
251    
252        private void testFailed() {
253            ++numErrors;
254    
255            try {
256                InputStream input = new BufferedInputStream(new FileInputStream(outputFile));
257                int length = input.available();
258                byte[] b = new byte[length];
259                input.read(b, 0, length);
260                writeTestProblems(testName, new String(b));
261            }
262            catch (IOException e) {
263                log.error("Failed to write test problems", e);
264            }
265        }
266    
267        private void writeTestProblems(String testName, String stdErr) {
268            long runTime = System.currentTimeMillis() - this.startTime;
269    
270            Xpp3Dom testCase = createTestElement("testcase", testName, runTime);
271            Xpp3Dom element = createElement(testCase, "failure");
272    
273            element.setAttribute("message", escapeAttribute(getMessage(stdErr)));
274            element.setAttribute("type", getType(stdErr));
275            element.setValue(stdErr);
276    
277            results.add(testCase);
278        }
279    
280        /**
281         * Adds system properties to the XML report.
282         */
283        private void showProperties(Xpp3Dom testSuite) {
284            Xpp3Dom properties = createElement(testSuite, "properties");
285    
286            Properties systemProperties = System.getProperties();
287    
288            if (systemProperties != null) {
289                Enumeration propertyKeys = systemProperties.propertyNames();
290    
291                while (propertyKeys.hasMoreElements()) {
292                    String key = (String) propertyKeys.nextElement();
293                    String value = systemProperties.getProperty(key);
294    
295                    if (value == null) {
296                        value = "null";
297                    }
298    
299                    Xpp3Dom property = createElement(properties, "property");
300    
301                    property.setAttribute("name", key);
302                    property.setAttribute("value", escapeAttribute(value));
303                }
304            }
305        }
306    
307        private static String escapeAttribute(String attribute) {
308            // Shouldn't Xpp3Dom do this itself?
309            String s = StringUtils.replace(attribute, "<", "&lt;");
310            return StringUtils.replace(s, ">", "&gt;");
311        }
312    
313        private Iterator getResults() {
314            return results.iterator();
315        }
316    
317        private String elapsedTimeAsString(long runTime) {
318            return numberFormat.format((double) runTime / MS_PER_SEC);
319        }
320    
321        /**
322         * Gets the messages following the exception type.
323         */
324        private String getMessage(String stdErr) {
325            int beginMarker = stdErr.indexOf("Exception:") + 10;
326            int endMarker = stdErr.indexOf("\n", beginMarker);
327            return stdErr.substring(beginMarker, endMarker);
328        }
329    
330        /**
331         * Gets the type of exception from the stacktrace.
332         */
333        private String getType(String stdErr) {
334            int endMarker = stdErr.indexOf("Exception:") + 9;
335            int beginMarker = stdErr.lastIndexOf("\n", endMarker) + 1;
336            return stdErr.substring(beginMarker, endMarker);
337        }
338    
339        /** 
340         * The generated reports xml file for surefire..
341         * 
342         * @return generated reports xml file
343         */
344        public File getReportsFile() {
345            return this.reportFile;
346        }
347    
348        /**
349         * The text file which holds the stdout or stderr.
350         *
351         * @return File
352         */
353        public File getOutputFile() {
354            return this.outputFile;
355        }
356    }