001    package org.hackystat.sensor.ant.junit;
002    
003    import java.io.File;
004    import java.util.ArrayList;
005    import java.util.Date;
006    import java.util.HashMap;
007    import java.util.List;
008    import java.util.Map;
009    
010    import javax.xml.bind.JAXBContext;
011    import javax.xml.bind.JAXBException;
012    import javax.xml.bind.Unmarshaller;
013    import javax.xml.datatype.XMLGregorianCalendar;
014    
015    import org.apache.tools.ant.BuildException;
016    import org.hackystat.sensor.ant.junit.jaxb.Error;
017    import org.hackystat.sensor.ant.junit.jaxb.Failure;
018    import org.hackystat.sensor.ant.junit.jaxb.Testcase;
019    import org.hackystat.sensor.ant.junit.jaxb.Testsuite;
020    import org.hackystat.sensor.ant.task.HackystatSensorTask;
021    import org.hackystat.sensor.ant.util.JavaClass2FilePathMapper;
022    import org.hackystat.sensor.ant.util.LongTimeConverter;
023    import org.hackystat.sensorshell.SensorShellException;
024    
025    /**
026     * Implements an Ant task that parses the XML files generated by JUnit and sends the test case
027     * results to the Hackystat server.
028     * 
029     * You can specify the location of the source files either through the 'sourcePath' attribute or the
030     * 'srcPath' nested element. I agree, this isn't optimal, but I'm going for backward compatibility
031     * at the moment. Eventually, we probably want to get rid of the sourcePath attribute option.
032     * 
033     * @author Philip Johnson, Hongbing Kou, Joy Agustin, Julie Ann Sakuda, Aaron A. Kagawa
034     */
035    public class JUnitSensor extends HackystatSensorTask {
036    
037      /** The class to file path mapper. */
038      private JavaClass2FilePathMapper javaClass2FilePathMapper;
039    
040      /** The name of this tool. */
041      private static String tool = "JUnit";
042    
043      /** Initialize a new instance of a JUnitSensor. */
044      public JUnitSensor() {
045        super(tool);
046      }
047    
048      /**
049       * Initialize a new instance of a JUnitSensor, passing the host email, and password directly. This
050       * supports testing. Note that when this constructor is called, offline data recovery by the
051       * sensor is disabled.
052       * 
053       * @param host The hackystat host URL.
054       * @param email The Hackystat email to use.
055       * @param password The Hackystat password to use.
056       */
057      public JUnitSensor(String host, String email, String password) {
058        super(host, email, password, tool);
059      }
060    
061      /**
062       * Parses the JUnit XML files and sends the resulting JUnit test case results to the hackystat
063       * server. This method is invoked automatically by Ant.
064       * 
065       * @throws BuildException If there is an error.
066       */
067      @Override
068      public void executeInternal() throws BuildException {
069        setupSensorShell();
070        int numberOfTests = 0;
071        Date startTime = new Date();
072        // Iterate though each file, extract the JUnit data, send to sensorshell.
073        for (File dataFile : getDataFiles()) {
074          verboseInfo("Processing JUnit file: " + dataFile);
075          try {
076            numberOfTests += processJUnitXmlFile(dataFile);
077          }
078          catch (Exception e) {
079            signalError("Failure processing: " + dataFile, e);
080          }
081        }
082        this.sendAndQuit();
083        summaryInfo(startTime, "UnitTest", numberOfTests);
084      }
085    
086      /**
087       * Parses a JUnit XML file and sends the JUnitEntry instances to the shell.
088       * 
089       * @param xmlFile The XML file name to be processed.
090       * @exception BuildException if any error.
091       * @return The number of test cases in this XML file.
092       */
093      public int processJUnitXmlFile(File xmlFile) throws BuildException {
094        XMLGregorianCalendar runtimeGregorian = LongTimeConverter.convertLongToGregorian(this.runtime);
095        try {
096          JAXBContext context = JAXBContext
097              .newInstance(org.hackystat.sensor.ant.junit.jaxb.ObjectFactory.class);
098          Unmarshaller unmarshaller = context.createUnmarshaller();
099    
100          // One JUnit test suite per file
101          Testsuite suite = (Testsuite) unmarshaller.unmarshal(xmlFile);
102          String testClassName = suite.getName();
103          // The start time for all entries will be approximated by the XML file's last mod time.
104          // The shell will ensure that it's unique by tweaking the millisecond field.
105          long startTime = xmlFile.lastModified();
106          List<Testcase> testcases = suite.getTestcase();
107          for (Testcase testcase : testcases) {
108            // Test case name
109            String testCaseName = testcase.getName();
110    
111            // Get the stop time
112            double elapsedTime = testcase.getTime();
113            long elapsedTimeMillis = (long) (elapsedTime * 1000);
114    
115            // Make a list of error strings.
116            // This should always be a list of zero or one elements.
117            List<String> stringErrorList = new ArrayList<String>();
118            Error error = testcase.getError();
119            if (error != null) {
120              stringErrorList.add(error.getMessage());
121            }
122    
123            // Make a list of failure strings.
124            // This should always be a list of zero or one elements.
125            List<String> stringFailureList = new ArrayList<String>();
126            Failure failure = testcase.getFailure();
127            if (failure != null) {
128              stringFailureList.add(failure.getMessage());
129            }
130    
131            String result = "pass";
132            if (!stringErrorList.isEmpty() || !stringFailureList.isEmpty()) {
133              result = "fail";
134            }
135    
136            String name = testClassName + "." + testCaseName;
137            // Alter startTime to guarantee uniqueness.
138            long uniqueTstamp = this.tstampSet.getUniqueTstamp(startTime);
139    
140            // Get altered start time as XMLGregorianCalendar
141            XMLGregorianCalendar startTimeGregorian = LongTimeConverter
142                .convertLongToGregorian(uniqueTstamp);
143    
144            Map<String, String> keyValMap = new HashMap<String, String>();
145            keyValMap.put("Tool", "JUnit");
146            keyValMap.put("SensorDataType", "UnitTest");
147    
148            // Required
149            keyValMap.put("Runtime", runtimeGregorian.toString());
150            keyValMap.put("Timestamp", startTimeGregorian.toString());
151            keyValMap.put("Name", name);
152            keyValMap.put("Resource", testCaseToPath(testClassName));
153            keyValMap.put("Result", result);
154    
155            // Optional
156            keyValMap.put("ElapsedTime", Long.toString(elapsedTimeMillis));
157            keyValMap.put("TestName", testClassName);
158            keyValMap.put("TestCaseName", testCaseName);
159    
160            if (!stringFailureList.isEmpty()) {
161              keyValMap.put("FailureString", stringFailureList.get(0));
162            }
163    
164            if (!stringErrorList.isEmpty()) {
165              keyValMap.put("ErrorString", stringErrorList.get(0));
166            }
167    
168            this.sensorShell.add(keyValMap); // add data to sensorshell
169          }
170          return testcases.size();
171        }
172        catch (JAXBException e) {
173          throw new BuildException(errMsgPrefix + "Failure in JAXB " + xmlFile, e);
174        }
175        catch (SensorShellException f) {
176          throw new BuildException(errMsgPrefix + "Failure in SensorShell " + xmlFile, f);
177        }
178      }
179    
180      /**
181       * Maps the fully qualified test case class to its corresponding source file.
182       * 
183       * There are two ways to specify the source files: through the sourcePath attribute, which is a
184       * string containing the path to the source file directory structure, or through the srcPath
185       * nested element, which is a set of fileSets.
186       * 
187       * @param testCaseName Fully qualified test case name.
188       * @return The source file corresponding to this test case.
189       */
190      private String testCaseToPath(String testCaseName) {
191        JavaClass2FilePathMapper mapper = this.getJavaClass2FilePathMapper();
192        return mapper.getFilePath(testCaseName);
193      }
194    
195      /**
196       * Get a java class to file path mapper.
197       * 
198       * @return The mapper.
199       */
200      private JavaClass2FilePathMapper getJavaClass2FilePathMapper() {
201        if (this.javaClass2FilePathMapper == null) {
202          this.javaClass2FilePathMapper = new JavaClass2FilePathMapper(getSourceFiles());
203        }
204        return this.javaClass2FilePathMapper;
205      }
206    }