001 package org.hackystat.sensor.ant.emma; 002 003 import java.io.File; 004 import java.util.Date; 005 import java.util.HashMap; 006 import java.util.Map; 007 008 import javax.xml.bind.JAXBContext; 009 import javax.xml.bind.JAXBException; 010 import javax.xml.bind.Unmarshaller; 011 import javax.xml.datatype.XMLGregorianCalendar; 012 013 import org.apache.tools.ant.BuildException; 014 import org.hackystat.sensor.ant.emma.jaxb.All; 015 import org.hackystat.sensor.ant.emma.jaxb.Class; 016 import org.hackystat.sensor.ant.emma.jaxb.Coverage; 017 import org.hackystat.sensor.ant.emma.jaxb.Data; 018 import org.hackystat.sensor.ant.emma.jaxb.Package; 019 import org.hackystat.sensor.ant.emma.jaxb.Report; 020 import org.hackystat.sensor.ant.emma.jaxb.Srcfile; 021 import org.hackystat.sensor.ant.task.HackystatSensorTask; 022 import org.hackystat.sensor.ant.util.JavaClass2FilePathMapper; 023 import org.hackystat.sensor.ant.util.LongTimeConverter; 024 import org.hackystat.sensorshell.SensorShellException; 025 026 027 /** 028 * Implements an Ant task that parses the XML files generated by Emma, a Java coverage tool. 029 * The Ant Task sends the Coverage data to a Hackystat server. 030 * 031 * @author Aaron A. Kagawa, Cedric Qin Zhang, Philip Johnson 032 */ 033 public class EmmaSensor extends HackystatSensorTask { 034 035 /** The mapper used to map class names to file paths. */ 036 private JavaClass2FilePathMapper javaClass2FilePathMapper; 037 038 /** The name of this tool. */ 039 private static String tool = "Emma"; 040 041 /** Initialize a new instance of a EmmaSensor. */ 042 public EmmaSensor() { 043 super(tool); 044 } 045 046 /** 047 * Initialize a new instance of a EmmaSensor, passing the host and directory 048 * key in explicitly. This supports testing. Note that when this constructor 049 * is called, offline data recovery by the sensor is disabled. 050 * @param host The hackystat host URL. 051 * @param email The Hackystat email to use. 052 * @param password The Hackystat password to use. 053 */ 054 public EmmaSensor(String host, String email, String password) { 055 super(host, email, password, tool); 056 } 057 058 /** 059 * Parses the Coverage XML files and sends the resulting coverage results to 060 * the hackystat server. This method is invoked automatically by Ant. 061 * @throws BuildException If there is an error. 062 */ 063 @Override 064 public void executeInternal() throws BuildException { 065 this.setupSensorShell(); 066 int numberOfEntries = 0; 067 Date startTime = new Date(); 068 for (File dataFile : this.getDataFiles()) { 069 verboseInfo("Processing Emma file: " + dataFile); 070 try { 071 numberOfEntries += this.processCoverageXmlFile(dataFile); 072 } 073 catch (Exception e) { 074 signalError("Failure processing: " + dataFile, e); 075 } 076 } 077 this.sendAndQuit(); 078 summaryInfo(startTime, "Coverage", numberOfEntries); 079 } 080 081 /** 082 * Parses an Emma XML file and sends the data to the shell. The only coverage information that 083 * is used by the sensor is the Emma class level report. All other coverage information is 084 * ignored; for example the sensor does not use the method element coverage information. 085 * Instead, the sensor parses the class element. Here is an example: 086 * <pre> 087 * <class name="JUnitSensor"> 088 * <coverage type="class, %" value="100% (1/1)"/> 089 * <coverage type="method, %" value="58% (7/12)"/> 090 * <coverage type="block, %" value="57% (374/656)"/> 091 * <coverage type="line, %" value="58% (80.5/139)"/> 092 * </class> 093 * </pre> 094 * The granularities of class, method, block, and line are retrieved from the class element. 095 * One could dig down into the method elements, but we are not doing this at the moment. 096 * 097 * @param xmlFile The XML file name to be processed. 098 * @return The number of coverage entries in this XML file. 099 */ 100 public int processCoverageXmlFile(File xmlFile) { 101 XMLGregorianCalendar runtimeGregorian = LongTimeConverter.convertLongToGregorian(this.runtime); 102 // The start time for all entries will be approximated by the XML file's last mod time. 103 // The shell will ensure that it's unique by tweaking the millisecond field. 104 long startTime = xmlFile.lastModified(); 105 try { 106 JAXBContext context = 107 JAXBContext.newInstance(org.hackystat.sensor.ant.emma.jaxb.ObjectFactory.class); 108 Unmarshaller unmarshaller = context.createUnmarshaller(); 109 110 // emma report 111 Report report = (Report) unmarshaller.unmarshal(xmlFile); 112 Data data = report.getData(); 113 All allData = data.getAll(); 114 115 int coverageEntriesCount = 0; 116 for (Package packageReport : allData.getPackage()) { 117 String packageName = packageReport.getName(); 118 for (Srcfile srcfile : packageReport.getSrcfile()) { 119 for (Class classReport : srcfile.getClazz()) { 120 String className = classReport.getName(); 121 String javaClassName = packageName + '.' + className; 122 String javaSourceFilePath = 123 this.getJavaClass2FilePathMapper().getFilePath(javaClassName); 124 if (javaSourceFilePath == null) { 125 verboseInfo("Warning: Unable to find java source file path for class '" 126 + javaClassName + "'. Use empty string for file path."); 127 javaSourceFilePath = ""; 128 } 129 130 // Alter startTime to guarantee uniqueness. 131 long uniqueTstamp = this.tstampSet.getUniqueTstamp(startTime); 132 133 // Get altered start time as XMLGregorianCalendar 134 XMLGregorianCalendar startTimeGregorian = 135 LongTimeConverter.convertLongToGregorian(uniqueTstamp); 136 137 Map<String, String> keyValMap = new HashMap<String, String>(); 138 keyValMap.put("Tool", "Emma"); 139 keyValMap.put("SensorDataType", "Coverage"); 140 141 // Required 142 keyValMap.put("Runtime", runtimeGregorian.toString()); 143 keyValMap.put("Timestamp", startTimeGregorian.toString()); 144 keyValMap.put("Resource", javaSourceFilePath); 145 146 // Optional 147 keyValMap.put("ClassName", javaClassName); 148 149 150 for (Coverage coverage : classReport.getCoverage()) { 151 String type = coverage.getType(); 152 String granularity = type.substring(0, type.indexOf(", %")); 153 String value = coverage.getValue(); 154 String coveredString = value.substring(value.indexOf('(') + 1, value.indexOf('/')); 155 String totalString = value.substring(value.indexOf('/') + 1, value.indexOf(')')); 156 double covered = new Double(coveredString); 157 double total = new Double(totalString); 158 159 keyValMap.put(granularity + "_Covered", String.valueOf(covered)); 160 keyValMap.put(granularity + "_Uncovered", String.valueOf(total - covered)); 161 } 162 163 this.sensorShell.add(keyValMap); // add data to sensorshell 164 coverageEntriesCount++; 165 } 166 } 167 } 168 return coverageEntriesCount; 169 } 170 catch (JAXBException e) { 171 throw new BuildException(errMsgPrefix + "JAXB Problem " + xmlFile, e); 172 } 173 catch (SensorShellException e) { 174 throw new BuildException(errMsgPrefix + "Sensor processing problem " + xmlFile, e); 175 } 176 } 177 178 /** 179 * Get a java class to file path mapper. 180 * @return The mapper. 181 */ 182 private JavaClass2FilePathMapper getJavaClass2FilePathMapper() { 183 if (this.javaClass2FilePathMapper == null) { 184 this.javaClass2FilePathMapper = new JavaClass2FilePathMapper(this.getSourceFiles()); 185 } 186 return this.javaClass2FilePathMapper; 187 } 188 }