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.testsuite;
021    
022    import java.io.File;
023    import java.io.FileInputStream;
024    import java.io.FileOutputStream;
025    import java.io.IOException;
026    import java.text.NumberFormat;
027    import java.util.ArrayList;
028    import java.util.Iterator;
029    
030    import org.w3c.dom.Document;
031    import org.w3c.dom.Element;
032    import org.w3c.dom.Node;
033    import org.w3c.dom.NodeList;
034    import org.w3c.dom.Text;
035    import org.w3c.tidy.Tidy;
036    import org.xml.sax.ErrorHandler;
037    import org.xml.sax.InputSource;
038    import org.xml.sax.SAXException;
039    import org.xml.sax.SAXParseException;
040    
041    import javax.xml.transform.Transformer;
042    import javax.xml.transform.TransformerFactory;
043    import javax.xml.transform.TransformerException;
044    import javax.xml.transform.TransformerConfigurationException;
045    import javax.xml.transform.dom.DOMSource; 
046    import javax.xml.transform.stream.StreamResult;
047    
048    import org.codehaus.mojo.pluginsupport.MojoSupport;
049    import org.codehaus.mojo.pluginsupport.ant.AntHelper;
050    
051    import org.apache.maven.model.DistributionManagement;
052    import org.apache.maven.project.MavenProject;
053    import org.apache.maven.plugin.MojoExecutionException;
054    import org.apache.maven.plugin.MojoFailureException;
055    import org.apache.maven.settings.Settings;
056    import org.apache.maven.settings.Server;
057    
058    import org.codehaus.plexus.util.FileUtils;
059    
060    import org.apache.tools.ant.Project;
061    import org.apache.tools.ant.taskdefs.Property;
062    import org.apache.tools.ant.taskdefs.XmlProperty;
063    import org.apache.tools.ant.taskdefs.optional.ssh.Scp;
064    import org.apache.tools.ant.taskdefs.optional.ssh.SSHExec;
065    
066    /**
067     * Download the ResultsSummary.html file from the site url.
068     * Update it with success rate (in percentage) of the results from each of the top level testsuites.
069     * Upload the file back again.
070     * 
071     * @goal summarize
072     *
073     * @version $Rev: 706640 $ $Date: 2008-10-21 14:44:05 +0000 (Tue, 21 Oct 2008) $
074     */
075    public class ResultsSummaryMojo
076    extends MojoSupport {
077        /**
078         * @component
079         */
080        protected AntHelper ant;
081    
082        /**
083         * @parameter default-value="${project.build.directory}"
084         * @read-only
085         */
086        private File targetDirectory;
087    
088        /**
089        * The maven project.
090        *
091        * @parameter expression="${project}"
092        * @required
093        * @readonly
094        */
095        protected MavenProject project = null;
096    
097        /**
098        * The build settings.
099        *
100        * @parameter expression="${settings}" default-value="${settings}
101        * @required
102        * @readonly
103        */
104        protected Settings settings;
105    
106        /**
107         * The username
108         * 
109         * @parameter expression="${username}"
110         */
111        private String username;
112    
113        /**
114         * The password
115         * 
116         * @parameter expression="${password}"
117         */
118        private String password;
119    
120        /**
121         * The passphrase
122         * 
123         * @parameter expression="${passphrase}"
124         */
125        private String passphrase;
126    
127        /**
128         * The keyfile
129         * 
130         * @parameter expression="${keyfile}"
131         */
132        private String keyFile;
133    
134        /**
135         * The passphrase
136         * 
137         * @parameter expression="${buildNumber}"
138         */
139        private String buildNumber;
140    
141        /**
142         * show results for only these many tests.
143         * 
144         * @parameter expression="${numberShown}" default-value="8"
145         */
146        private int numberShown;
147    
148        private NumberFormat numberFormat = NumberFormat.getInstance();
149    
150        private static final int PCENT = 100;
151    
152        private final String resultsFileName = "ResultsSummary.html";
153    
154        private Server server = null;
155    
156        private Scp scp;
157        private SSHExec ssh;
158    
159        protected MavenProject getProject()
160        {
161            return project;
162        }
163    
164        protected void init() throws MojoExecutionException, MojoFailureException {
165            super.init();
166    
167            ant.setProject(getProject());
168    
169            String siteId = project.getDistributionManagement().getSite().getId();
170            server = settings.getServer(siteId);
171    
172            scp = (Scp)ant.createTask("scp");
173            scp.setKeyfile(getKeyFile());
174            scp.setPassword(getPassword());
175            scp.setPassphrase(getPassphrase());
176            scp.setTrust(true);
177    
178            ssh = (SSHExec)ant.createTask("sshexec");
179            ssh.setKeyfile(getKeyFile());
180            ssh.setPassword(getPassword());
181            ssh.setPassphrase(getPassphrase());
182            ssh.setTrust(true);
183    
184        }
185    
186        private String getKeyFile() {
187            if ( keyFile != null ) {
188                return keyFile;
189            }
190            else if ( server != null && server.getPrivateKey() != null ) {
191                return server.getPrivateKey();
192            }
193    
194            return "/home/" + getUsername() + "/.ssh/id_dsa";
195        }
196    
197        private String getUsername() {
198            if ( username != null ) {
199                return username;
200            }
201            else if ( server != null && server.getUsername() != null ) {
202                return server.getUsername();
203            }
204    
205            return System.getProperty("user.name");
206        }
207    
208        private String getPassword() {
209            if ( password != null ) {
210                return password;
211            }
212            else if ( server != null && server.getPassword() != null ) {
213                return server.getPassword();
214            }
215    
216            return " ";
217        }
218    
219        private String getPassphrase() {
220            if ( passphrase != null ) {
221                return passphrase;
222            }
223            else if ( server != null && server.getPassphrase() != null ) {
224                return server.getPassphrase();
225            }
226    
227            return " ";
228        }
229    
230        /**
231         * called by execute from super
232         */
233        protected void doExecute() throws Exception {
234            if ( buildNumber == null ) {
235                log.warn("No build number specified; returning");
236                return;
237            }
238    
239            File currentSiteDirectory = new File(targetDirectory, "/site");
240            if ( !currentSiteDirectory.exists() ) {
241                log.warn("No site directory here; returning");
242                return;
243            }
244    
245            // Download ResultsSummary.html and parse it.
246            File resultsFile = null;
247            try {
248                downloadHTML();
249                resultsFile = new File(targetDirectory, resultsFileName);            
250            }
251            catch ( Exception e ) {
252                log.warn("Download failed. " + e.getMessage());
253            }
254    
255            Tidy tidy = new Tidy();
256            tidy.setQuiet(true);
257            tidy.setShowWarnings(false);
258    
259            if ( resultsFile == null || !resultsFile.exists() ) {
260                log.info( resultsFileName + " could not be downloaded. Using the template to create anew");
261                resultsFile = new File(project.getBasedir(), "src/main/resources/" + resultsFileName);
262            }
263    
264            FileInputStream is = new FileInputStream( resultsFile );
265            Document document = tidy.parseDOM(is, null); 
266            is.close();
267    
268            File reportsDir = new File(targetDirectory, "surefire-reports");
269            if ( !reportsDir.exists() ) {
270                log.warn("No surefire-reports directory here");
271                return;
272            }
273    
274            ArrayList files = (ArrayList) FileUtils.getFiles(reportsDir, "TEST-*.xml", null, true);
275            if ( files.size() > 0 ) {
276                document = insertNewColumn(document);
277                if ( document == null ) {
278                    throw new MojoFailureException("Main table cannot be found in the " + resultsFileName + ". The file may be corrupted");
279                }
280            }
281    
282            for ( Iterator itr=files.iterator(); itr.hasNext(); ) {
283                File file = (File) itr.next();
284                log.debug("working on " + file.getAbsolutePath() );
285                document = processFile(document, file);
286            }
287    
288            // Use a Transformer for output
289            TransformerFactory tFactory = TransformerFactory.newInstance();
290            Transformer transformer = tFactory.newTransformer();
291    
292            // write the document back into a temporary file.
293            File tempFile = new File(targetDirectory, "ResultsSummary-2.html");
294            FileOutputStream os = new FileOutputStream( tempFile );
295            DOMSource source = new DOMSource(document);
296            StreamResult result = new StreamResult(os);
297            transformer.transform(source, result); 
298    
299            os.flush();
300            os.close();
301    
302            // tidy the document and create/replace ResultsSummary.html in the target directory
303            resultsFile = new File(targetDirectory, resultsFileName);
304            is = new FileInputStream( tempFile );
305            os = new FileOutputStream( resultsFile );
306            tidy.parse(is, os);
307            is.close();
308            os.close();
309    
310            // delete the temp file.
311            tempFile.delete();
312    
313            try {
314                uploadHTML(resultsFile);
315            }
316            catch ( Exception e ) {
317                log.warn("Upload failed. " + e.getMessage());
318            }
319        }
320    
321    
322        private String getRemoteUri() {
323            String siteUri = project.getDistributionManagement().getSite().getUrl();
324    
325            // chop off the protocol
326            int index = siteUri.indexOf("://");
327            siteUri = siteUri.substring(index + 3);
328            log.debug("siteUri uri is " + siteUri);
329    
330            // chop off the buildNumber directory at the end. This is used to deploy site files.
331            index = siteUri.lastIndexOf("/");
332            siteUri = siteUri.substring(0, index);
333            log.debug("siteUri uri is " + siteUri);
334    
335            // insert : between the host and path
336            index = siteUri.indexOf("/");
337            String remoteUri = siteUri.substring(0, index) + ":" + siteUri.substring(index);
338            log.debug("siteUri uri is " + remoteUri);
339    
340    
341            // construct the uri using username
342            remoteUri = getUsername() + ":" + getPassword() + "@" + remoteUri;
343            log.info("Remote uri is " + remoteUri);
344    
345            return remoteUri;
346    
347        }
348    
349    
350        /**
351         * Download the html from the remote site where it is has been deployed.
352         */
353        private void downloadHTML()
354        {
355            String remoteUri = getRemoteUri() + "/" + resultsFileName;
356    
357            scp.setFile(remoteUri);
358            scp.setTodir(targetDirectory.getAbsolutePath());
359            scp.execute();
360        }
361    
362    
363        /**
364         * Upload the html to the remote site where it will be deployed.
365         */
366        private void uploadHTML(File resultsFile)
367        {
368            String remoteUri = getRemoteUri();
369            scp.setFile( resultsFile.getAbsolutePath() );
370            scp.setTodir(remoteUri);
371            scp.execute();
372    
373            // Use the following block to set 664 perms on the uploaded html file; else synch will fail.
374            // ssh is setting the right perms but blocking. setTimeout doesn't seem to work.
375            /*
376            remoteUri = remoteUri + "/" + resultsFileName;
377            int atindex = remoteUri.lastIndexOf("@");
378            int index = remoteUri.lastIndexOf(":");
379            ssh.setHost(remoteUri.substring(atindex+1, index));
380            ssh.setUsername(getUsername());
381            ssh.setCommand("chmod 664 " + remoteUri.substring(index+1));
382            ssh.setTimeout(30 * 1000);
383            ssh.execute();
384            */
385        }
386    
387    
388        /**
389         * Append a new column for the latest build. Put the build number in the column header
390         */
391        private Document insertNewColumn(Document document)
392        {
393            Element table = getElementById(document.getDocumentElement(), "table", "mainTable");
394            if ( table == null ) {
395                log.info("table is null");
396                return null;
397            }
398    
399            Element thead = getElementById(table, "thead", "mainTableHead");
400            Element tr = (Element) thead.getFirstChild();
401    
402            Element td= document.createElement("TD");
403            td.setAttribute("class", "servers");
404    
405            Element anchor = document.createElement("a");
406            anchor.setAttribute("href", "./" + buildNumber + "/surefire-report.html");
407            Text text = document.createTextNode(buildNumber);
408            anchor.appendChild(text);
409    
410            td.appendChild(anchor);
411            tr.appendChild(td);
412    
413            // increment the cols attribute for the table
414            int cols = tr.getChildNodes().getLength();
415    
416            // check for number of columns to be shown. 
417            // Don't take the suite names column into count.
418            if ( cols > (numberShown + 1) ) {
419                cols = cleanup(table);
420            }
421    
422            table.setAttribute("cols", String.valueOf(cols) );
423    
424    
425            return document;
426        }
427    
428        private Document processFile(Document document, File file)
429        {
430            String pcent = getResultsFromFile(file);
431    
432            // strip off TEST- and .xml from the filename to get the suitename
433            String fileName = FileUtils.basename(file.getName());
434            fileName = fileName.substring(fileName.indexOf("-") + 1);
435            fileName = fileName.substring(0, fileName.length()-1);
436            document = insertColumn(document, pcent, fileName);
437    
438            return document;
439        }
440    
441        /**
442         * Load the surefire-report xml file as an ANT xml property and get the values of the results
443         * compute percentage
444         */
445        private String getResultsFromFile(File xmlFile)
446        {
447            String prefix = String.valueOf(System.currentTimeMillis());
448            loadXMLProperty(xmlFile, prefix);
449    
450            String tests = ant.getAnt().getProperty(prefix + ".testsuite.tests");
451            String errors = ant.getAnt().getProperty(prefix + ".testsuite.errors");
452            String failures = ant.getAnt().getProperty(prefix + ".testsuite.failures");
453            String skipped = ant.getAnt().getProperty(prefix + ".testsuite.skipped");
454    
455            log.debug("tests: " + tests + "; errors:" + errors + "; failures:" + failures + "; skipped:" + skipped);
456    
457            int testsNum = Integer.parseInt(tests);
458            int errorsNum = Integer.parseInt(errors);
459            int failuresNum = Integer.parseInt(failures);
460            int skippedNum = Integer.parseInt(skipped);
461    
462            //String pcent = tests + "/" + errors + "/" + failures + " (" + computePercentage(testsNum, errorsNum, failuresNum, skippedNum) + "%)";
463            String pcent = tests + "/" + errors + "/" + failures; 
464            return pcent;
465        }
466    
467        /**
468         * http://ant.apache.org/manual/CoreTasks/xmlproperty.html
469         */
470        private void loadXMLProperty(File src, String prefix)
471        {
472            XmlProperty xmlProperty = (XmlProperty)ant.createTask("xmlproperty");
473            xmlProperty.setFile(src);
474            if ( prefix != null ) {
475                xmlProperty.setPrefix(prefix);
476            }
477            xmlProperty.setCollapseAttributes(true);
478            xmlProperty.execute();
479            log.debug("Loaded xml file as ant property with prefix " + prefix);
480        }
481    
482        /**
483         * compute percentage
484         */
485        public String computePercentage( int tests, int errors, int failures, int skipped )
486        {
487            float percentage;
488            if ( tests == 0 ) {
489                percentage = 0;
490            }
491            else {
492                percentage = ( (float) ( tests - errors - failures - skipped ) / (float) tests ) * PCENT;
493            }
494    
495            return numberFormat.format( percentage );
496        }
497    
498        /**
499         * Insert the rest of the column. If there is no matching row for the suite name, create a new row.
500         */
501        private Document insertColumn(Document document, String pcent, String suiteName)
502        {
503            log.debug("inserting column");
504    
505            Element table = getElementById(document.getDocumentElement(), "table", "mainTable");
506            int cols = Integer.parseInt( table.getAttribute("cols") );
507    
508            Element tr = getElementById(table, "tr", suiteName);
509    
510            if ( tr != null ) {
511                // creating empty cells in the cols for the previous failed builds.
512                NodeList nodeList = tr.getElementsByTagName("td");
513                Element td;
514                for ( int i=nodeList.getLength()+1; i<cols; i++ ) {
515                    td = document.createElement("TD");
516                    td.setAttribute("class", "cell");                
517                    tr.appendChild(td);
518                }
519    
520                td = document.createElement("TD");
521                td.setAttribute("class", "cell");
522    
523                Element anchor = document.createElement("a");
524                anchor.setAttribute("href", "./" + buildNumber + "/" + suiteName + "/surefire-report.html");
525                Text text = document.createTextNode(pcent);
526                anchor.appendChild(text);
527    
528                td.appendChild(anchor);
529                tr.appendChild(td);
530            }
531            else {
532                log.debug("Creating a new row for a new suite");
533                tr = document.createElement("TR");
534                tr.setAttribute("id", suiteName);
535    
536                Element td = document.createElement("TD");
537                td.setAttribute("class", "suite");
538                Text text = document.createTextNode(suiteName);
539                td.appendChild(text);
540                tr.appendChild(td);
541    
542                // creating empty cells in the cols for the previous builds.
543                for ( int i=1; i<cols; i++ ) {
544                    td = document.createElement("TD");
545                    td.setAttribute("class", "cell");                
546                    tr.appendChild(td);
547                }
548    
549                Element anchor = document.createElement("a");
550                anchor.setAttribute("href", "./" + buildNumber + "/" + suiteName + "/surefire-report.html");
551                text = document.createTextNode(pcent);
552                anchor.appendChild(text);
553                td.appendChild(anchor);
554    
555                table.appendChild(tr);
556            }
557    
558            log.debug("inserted column");
559    
560            return document;
561        }
562    
563        /**
564         * Get a child element identified by an ID
565         */
566        private Element getElementById(Element element, String tagName, String id)
567        {
568            log.debug("Searching for tag " + tagName + " with id=" + id);
569    
570            Element foundElement = null;
571    
572            NodeList nodeList = element.getElementsByTagName(tagName);
573    
574            for ( int i=0; i<nodeList.getLength(); i++ ) {
575                foundElement = (Element) nodeList.item(i);
576                log.debug("Element is " + foundElement.getTagName() + " " + foundElement.getAttribute("id") );
577                if ( id.trim().equals(foundElement.getAttribute("id").trim()) ) {
578                    break;
579                }
580                else {
581                    foundElement = null;
582                }
583            }
584    
585            return foundElement;
586        }
587    
588        /**
589         * Removes the oldest test column(s) from the table based on the value set in 'numberShown' variable
590         */
591        private int cleanup(Element table)
592        {
593            log.info("Removing oldest column");
594    
595            NodeList nodeList = table.getElementsByTagName("tr");
596    
597            int new_cols = 0;
598            for ( int i=0; i<nodeList.getLength(); i++ ) {
599                Element tr = (Element) nodeList.item(i);
600                Element suiteColumn = (Element) tr.getFirstChild();
601                Element removeMe = (Element) suiteColumn.getNextSibling();
602                tr.removeChild(removeMe);
603    
604                // get the count from just the header row since only the header has been added yet
605                if ( i==0 ) {
606                    new_cols = tr.getChildNodes().getLength();
607                }
608            }
609    
610            if ( new_cols > (numberShown + 1) ) {
611                new_cols = cleanup(table);
612            }
613    
614            log.debug( String.valueOf("Returning cols: " + new_cols) );
615    
616            return new_cols;
617        }
618    }