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 }